マキモノ
Web開発無料✅ 公式検証済みv1.0.0 / 更新

社内名簿シートから選択式スタッフ入力を作る — メールをブラウザに出さない Next.js 設計

認証なしの公開業務アプリで、手入力の氏名を名簿由来のプルダウンに置き換えつつ、個人情報をクライアントへ配らない設計と検証手順。Server Component で {id,name} だけを渡し、サーバが id から氏名とメールを解決する。配信物へのメール混入を grep で実測する検証と、実際に踏んだ落とし穴7件つき。

出品者: nishi@orgiast.jp📖 読込 約3,515トークン (約5円)💰 コスパ 11
トークン節約メーター89%節約
ゼロからAIに作らせた場合4.5万トークン
このMDを読ませた場合5,000トークン

4万トークンの節約 (API料金換算で約60円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。

この巻物について

社内名簿シートから選択式スタッフ入力を作る — メールをブラウザに出さない Next.js 設計」は、Web開発カテゴリのAI指示書(MDファイル)です。認証なしの公開業務アプリで、手入力の氏名を名簿由来のプルダウンに置き換えつつ、個人情報をクライアントへ配らない設計と検証手順。Server Component で {id,name} だけを渡し、サーバが id から氏名とメールを解決する。配信物へのメール混入を grep で実測する検証と、実際に踏んだ落とし穴7件つき。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約4万トークン(API料金換算で約60円)・89%のトークンを節約できます。

カテゴリ
Web開発
対応AI
claude-code、cursor、codex-cli
ライセンス
商用利用可 (再販不可)
価格
無料
ゼロから開発時
約4.5万トークン
この巻物使用時
約5,000トークン
節約量
約4万トークン (約60円)
更新日
2026-09-01

使い方 (AIに渡す3つの方法)

いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。

⬇ .md をダウンロード
claude "https://makimono-md.vercel.app/api/v1/files/next-js/raw を読み込んで、この指示書どおりに実装して"
claude-codecursorcodex-cliライセンス: 商用利用可 (再販不可)

中身

社内名簿シートから「選択式スタッフ入力」を作る — メールアドレスをブラウザに出さない Next.js 設計

これは何のための指示書か

ログイン機構を持たない業務アプリ(現場のスタッフが URL を開いてすぐ使う類)で、 送信者を手入力させるのをやめて、社内名簿から生成したプルダウンに置き換えるための設計と検証手順。

手入力は表記揺れ(山田花子 / 山田 花子 / やまだ)で集計できなくなるので選択式にしたい。 一方で、名簿にはメールアドレスという個人情報が入っている。認証の無い公開URLのアプリに 名簿をそのまま持ち込むと、ブラウザに全員分のメールアドレスが配られてしまう。

この指示書は「選択式にする」と「メールアドレスをブラウザに出さない」を同時に満たす。

前提と入力

  • フレームワーク: Next.js App Router(Server Component が使えること)。同等の SSR フレームワークでも考え方は同じ。
  • 名簿の形: email,role,name,updatedAt のような CSV(スプレッドシートからエクスポートできるもの)。 数千行あり、毎月更新される
  • 抽出条件の例: role が特定の値、または name が特定の接頭辞(例 <接頭辞>)で始まる人だけを対象にする。 接頭辞は表示時に除去する。

絶対に守る設計ルール

  1. 氏名をコードに書かない。 名簿は毎月変わる。CSV から生成物(config/staff.json)を作るスクリプトを用意し、 コードには条件だけを書く。ハードコードすると更新のたびに人が編集することになる。
  2. メールアドレスをクライアントへ渡さない。 Server Component が名簿を読み、 クライアントには { id, name } だけを渡す。送信も id のみ。サーバが id から氏名とメールを解決する。 これにより「クライアントの申告を信用しない」も同時に達成できる(なりすまし送信を弾ける)。
  3. id は安定した非可逆値にする。sha256(小文字化したメール).slice(0, 12) のように、 同じ人なら再生成しても同じ id になる値を使う。行番号のような並び順依存の値は使わない。
  4. 生成物に毎回変わる値(生成時刻など)を入れない。 差分が安定し、レビューしやすくなる。

実装

1. 純関数モジュール lib/staff.js

import { createHash } from "node:crypto";

const PREFIX_RE = /^(<接頭辞>)/;

export function parseStaffCsv(csv) {
  const lines = String(csv).split(/\r\n|\r|\n/);
  let h = 0;
  while (h < lines.length && lines[h].trim() === "") h++;
  const cols = lines[h].split(",").map((c) => c.trim());
  const idx = {};
  cols.forEach((n, i) => (idx[n] = i));
  for (const req of ["email", "role", "name"]) {
    if (!(req in idx)) throw new Error(`CSVヘッダに "${req}" 列がありません`);
  }
  // 必須列だけで行の有効性を判定する(後述の落とし穴1)
  const maxRequired = Math.max(idx.email, idx.role, idx.name);

  const rows = [];
  for (let i = h + 1; i < lines.length; i++) {
    if (lines[i].trim() === "") continue;
    const c = lines[i].split(",").map((s) => s.trim());
    if (c.length <= maxRequired) continue;
    rows.push({ email: c[idx.email], role: c[idx.role], name: c[idx.name] });
  }
  return rows;
}

export const isTarget = (row) =>
  (row.role || "").trim() === "<対象ロール>" || PREFIX_RE.test(row.name || "");

export const toDisplayName = (name) =>
  String(name || "").replace(/^(<接頭辞>)[  ]*/, "").trim();

export const staffId = (email) =>
  createHash("sha256").update(String(email || "").trim().toLowerCase()).digest("hex").slice(0, 12);

export function buildStaffList(rows) {
  const seen = new Set();
  const list = [];
  for (const r of rows.filter(isTarget)) {
    const key = (r.email || "").trim().toLowerCase();
    if (seen.has(key)) continue;
    seen.add(key);
    list.push({
      id: staffId(r.email),
      name: toDisplayName(r.name),
      email: r.email,
      role: r.role,
      isPrefixed: PREFIX_RE.test(r.name || ""),
    });
  }
  return list.sort((a, b) =>
    a.isPrefixed !== b.isPrefixed ? (a.isPrefixed ? -1 : 1) : a.name.localeCompare(b.name, "ja")
  );
}

export const findStaffById = (list, id) => (id ? list.find((s) => s.id === id) : undefined);

2. 生成スクリプト scripts/gen-staff.mjs

node scripts/gen-staff.mjs --csv <CSVパス> [--out config/staff.json] [--expect-count N]
  • ロジックは lib/staff.js を使い、二重に書かない。
  • 0件なら exit 1(静かに空リストを出すと、プルダウンが空のまま本番に出る)。
  • --expect-count 不一致なら実際の件数を出して exit 1
  • 標準出力に内訳(総行数・条件別件数)を出す。メールアドレスは絶対に出力しない

3. ページを Server / Client に分ける

// app/page.js(Server Component。"use client" を付けない)
import staff from "../config/staff.json";
import HomeClient from "./home-client";

export default function Home() {
  // email はクライアントに渡さない
  return <HomeClient staff={staff.map((s) => ({ id: s.id, name: s.name, isPrefixed: s.isPrefixed }))} />;
}
// app/home-client.js("use client")
// <select id="staff-select"> に <optgroup> で2グループ。先頭に「選択してください」を置く。
// 選んだら localStorage に id と表示名を保存する。
// - 旧仕様(手入力の名前)のキーが残っていたら、名簿の name と完全一致すれば id に移行し、旧キーは削除する
// - 保存済み id が名簿に無ければ(退職等)選択なしに戻す
// - staff が0件なら「一覧が未生成です」と出す(真っ白にしない)

4. API 側でサーバが権威を持つ

const staffId = formData.get("staffId");
const staff = findStaffById(STAFF, staffId);
if (!staff) return json({ error: "スタッフを選択してください" }, 400);
// 以降 name は staff.name を使う。クライアントの name は受け取らない

検証(ここまでやって初めて「できた」と言える)

  1. 単体テストnode --test): ヘッダ列順を入れ替えた CSV / CRLF / 空行 / 必須列欠落で throw / 接頭辞除去 / id の安定性と一意性 / 重複排除 / 並び順。フィクスチャのメールは example.com の架空アドレスにする。
  2. ビルドして、生成物にメールが混入していないことを grep で実測する(推測で済ませない):
grep -rl -E "@[a-z0-9.-]+\.(com|jp|net)" .next/static/   # 0件が正解
grep -c  -E "@"  .next/server/app/index.html             # 0件が正解
  1. 実ブラウザでプルダウンを開き、option の件数と表示名を機械的に取り出して期待値と比べる (スクリーンショットの目視だけで終わらせない):
const groups = await page.evaluate(() => {
  const sel = document.getElementById("staff-select");
  return [...sel.querySelectorAll("optgroup")].map((og) => ({
    label: og.label,
    names: [...og.querySelectorAll("option")].map((o) => o.textContent),
  }));
});
  1. 本番でも同じ grep を回す(配信されている HTML と、参照されている JS チャンク全部):
for u in $(curl -s "$PROD" | grep -o -E '/_next/static/[^"]+\.js' | sort -u); do
  echo "$(curl -s "$PROD$u" | grep -c '@example')  $u"
done
  1. なりすましを弾けるかを実際に叩いて確認する: staffId に存在しない値 → 400 / 旧仕様の name だけ送信 → 400。

落とし穴(実際に踏んだもの)

  1. 行の有効性判定に「最後の任意列」を含めると、スタッフが静かに消える。 updatedAt のような末尾列を必須扱いにすると、その列が欠けた行が「列数不足」で捨てられ、 プルダウンから人が消える。必須列だけで判定する。
  2. CSV を API 経由で読むと途中で切り詰められることがある。 「本文をテキストで返す」系のAPIは大きいファイルを黙って truncate する。 件数を数えて期待値と比べ、必要ならエクスポート(CSV ダウンロード)経路に切り替える。
  3. localeCompare("ja") は漢字を読み順に並べない。 ふりがな列が無い名簿では読み順ソートは実現できない。 件数が少なければ割り切るか、名簿側の掲載順を採用する。「五十音順にした」と説明しない。
  4. 公開URLのアプリでは、生成物を private リポに置いてもブラウザ側は別問題。 リポジトリの秘匿性と、配信物に混入していないことは別々に確認する
  5. 人が見るチャンネル(チャット等)へ通知する機能を手で叩いて試すときは、投稿を自分で消せる経路だけを使う。 検証スクリプト側で「投稿 → 読み返して検証 → 削除」まで面倒を見る。 Windows で curl -F "field=日本語" を使うと argv が OS のコードページに変換されて渡り、 サーバには文字化けした文字列が届く。その結果プレースホルダ検知などの送信ガードをすり抜けて 実投稿が成立する。テスト文字列は Node の FormData で組む。
  6. サーバレスのリクエスト本文上限(数MB)は、アプリ側の添付上限より小さいことがある。 アプリが 8MB を許しても、プラットフォームが 4.5MB で弾けば親切なエラーは出ずに失敗する。 クライアント側で縮小するのが本来の対策。
  7. 環境変数を追加・変更したら必ず再デプロイする。 追加前に作られたデプロイには焼かれていない。

完成の定義

  • プルダウンに期待した人数だけが出て、接頭辞が除去されている(実ブラウザで機械的に確認済み)
  • 単体テスト全pass・ビルド成功・実ブラウザの通しテスト全pass
  • 配信物(ローカルビルドと本番の両方)にメールアドレスが1件も含まれないことを grep で確認済み
  • 存在しない id と旧仕様の送信が 400 で弾かれることを実際に叩いて確認済み
  • 名簿更新の手順が1コマンドに落ちている(人が氏名を編集する箇所が無い)

よくある質問

「社内名簿シートから選択式スタッフ入力を作る — メールをブラウザに出さない Next.js 設計」とは何ですか?

認証なしの公開業務アプリで、手入力の氏名を名簿由来のプルダウンに置き換えつつ、個人情報をクライアントへ配らない設計と検証手順。Server Component で {id,name} だけを渡し、サーバが id から氏名とメールを解決する。配信物へのメール混入を grep で実測する検証と、実際に踏んだ落とし穴7件つき。

どれくらいトークン(費用)を節約できますか?

ゼロから開発すると約4.5万トークンかかりますが、この巻物を使えば約5,000トークンで済みます。差し引き約4万トークン(API料金換算で約60円)・89%の節約です。

どうやって使いますか?

無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。

どのAIツールに対応していますか?

claude-code、cursor、codex-cli に対応しています。

商用利用できますか?

ライセンスは「商用利用可 (再販不可)」です。

🤝 自分でAIを動かすのは、まだ不安…という方へ

この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。

AI代行堂を見る →

関連する巻物

この巻物、誰かのトークンも救えます

𝕏 で節約レシートをシェア