一覧が表記ゆれで割れるのを、DBを書き換えずに直す(集計キー正規化+付け替えUI)
大小文字・全半角の違いだけで同じグループが複数カードに割れる問題を、DBの生値を保ったまま表示側の正規化キーで統合する。集計だけ直してフィルタを直さず「クリックしたら0件」になる最頻出の落とし穴、ページング・候補取得上限の罠、間違ったグループに入った行を後から移す付け替えUIまで含む。
約11.5万トークンの節約 (API料金換算で約170円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「一覧が表記ゆれで割れるのを、DBを書き換えずに直す(集計キー正規化+付け替えUI)」は、Web開発カテゴリのAI指示書(MDファイル)です。大小文字・全半角の違いだけで同じグループが複数カードに割れる問題を、DBの生値を保ったまま表示側の正規化キーで統合する。集計だけ直してフィルタを直さず「クリックしたら0件」になる最頻出の落とし穴、ページング・候補取得上限の罠、間違ったグループに入った行を後から移す付け替えUIまで含む。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約11.5万トークン(API料金換算で約170円)・82%のトークンを節約できます。
- カテゴリ
- Web開発
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約14万トークン
- この巻物使用時
- 約2.5万トークン
- 節約量
- 約11.5万トークン (約170円)
- 更新日
- 2026-09-25
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/db-ui/raw を読み込んで、この指示書どおりに実装して"
中身
一覧が表記ゆれで割れるのを、DBを書き換えずに直す(集計キー正規化 + 付け替えUI)
この指示書で直る症状
一覧画面のカードやグループが、同じものなのに表記の違いだけで複数に割れている。
サステナブル経営WEEK 1件
サステナブル経営week 8件 ← 本当は同じ。合計9件で1枚のはず
原因は、グループ化のキーにユーザーが入力した生の文字列をそのまま使っていること。 大文字小文字・全角半角・前後の空白・連続スペースが違うだけで別グループになる。
「DB を一括 UPDATE して表記を揃える」で直したくなるが、それはやらない。 元の入力を失うと、誤変換したときに戻せないし、外部システムと突き合わせる時に困る。 表示側の正規化だけで解決する。
設計(これをそのまま実装させてよい)
1. 正規化キーの関数を1つだけ作る
<lib>/grouping-key.ts のような共有モジュールに置く。
複数箇所にコピペすると、片方だけ直して壊れる(後述の落とし穴1がまさにそれ)。
/** グループ化のキー。表記ゆれ(大小文字・全半角・空白)を吸収する。 */
export function groupingKey(raw: string | null | undefined): string {
return raw == null
? ""
: String(raw).normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
}
normalize("NFKC")が全角英数を半角にする(WEEK→WEEK)。これを入れ忘れると全角が別グループに残る。\s+は全角スペースにもマッチする(JS の\sは U+3000 を含む)。toLowerCase()は最後。NFKC の後にやること。
2. 代表表示名を決める
統合したカードに何と表示するか。件数が最も多い生値を採用する(同数なら新しい方)。 少数派の表記に寄せると「見慣れない名前になった」と現場が混乱する。
export function pickDisplayName(
variants: { value: string; count: number; latest: string }[],
): string {
return [...variants]
.sort((a, b) => b.count - a.count || b.latest.localeCompare(a.latest))[0]?.value ?? "";
}
3. 集計する
export type Row = { groupField: string | null; createdAt: string | null };
export type Group = { key: string; name: string; count: number; latest: string };
export function groupRows(rows: Row[]): Group[] {
const groups = new Map<string, Map<string, { value: string; count: number; latest: string }>>();
for (const row of rows) {
const key = groupingKey(row.groupField);
if (!key) continue; // 空欄は「未分類」として別扱い
const value = row.groupField!;
const variants = groups.get(key) ?? new Map();
const v = variants.get(value) ?? { value, count: 0, latest: "" };
v.count++;
if ((row.createdAt ?? "") > v.latest) v.latest = row.createdAt ?? "";
variants.set(value, v);
groups.set(key, variants);
}
return [...groups].map(([key, byValue]) => {
const variants = [...byValue.values()];
return {
key,
name: pickDisplayName(variants),
count: variants.reduce((s, v) => s + v.count, 0),
latest: variants.reduce((m, v) => (v.latest > m ? v.latest : m), ""),
};
}).sort((a, b) => b.latest.localeCompare(a.latest));
}
4. 取込側でも既存表記に寄せる
外部フォーム・CSV・API から新しい行が入るなら、入る時点で既存の代表表記に揃える。
既存の値一覧を引いて groupingKey が一致するものがあれば、その代表表記を使う。
これをやらないと、直した先からまた新しいゆれが増える。
落とし穴(ここで必ず1回壊れる)
落とし穴1: 集計だけ直してフィルタを直さない ← 最頻出
一覧カードは /list?group=<名前> のようなリンクになっていることが多い。
集計側だけ正規化すると、カードをクリックした先が 0 件になる。
カードの表示名は代表表記(例 サステナブル経営week)だが、
絞り込み先の行には少数派表記(サステナブル経営WEEK)が混ざっているため、
生値の完全一致では拾えない。
→ 絞り込み側も同じ groupingKey に通す。クエリ文字列は生の名前のままでよく、
サーバ側で groupingKey(query) === groupingKey(row.groupField) で突き合わせる。
行数が数千程度なら DB の ilike に頼らず取得後に JS で比較してよい。
落とし穴2: ページングの前に絞り込む
「1ページ目を取得してから正規化で絞る」と、2ページ目以降の該当行が落ちる。 正規化での絞り込みを済ませてからページ分割する。
落とし穴3: 候補取得が上限で切れる
多くの DB クライアントは1回のクエリで 1000 行など上限がある。 既存の値一覧を引くところは 必ずページングで全件読む。 途中で切れると「既存表記に寄せる」が効かず、ゆれが増え続ける。 取得に失敗した時は空リストとして続行しない(黙ってゆれを増やすため)。エラーで止める。
落とし穴4: 生値を書き換えたくなる
「一度きれいにすれば終わり」と一括 UPDATE したくなるが、やらない。 表示側の正規化はいつでも戻せるのが最大の利点。
セットで作る「付け替えUI」
表記ゆれが直っても、そもそも間違ったグループに入った行は残る。 グループ項目を画面から編集できるようにする。
- 詳細画面でグループ項目を編集可能にする。
入力は 既存の代表表示名を候補に出す combobox(HTML の
datalistで十分)。自由入力も許可。 候補を出さないと、また新しい表記ゆれが増える。 - 一覧にチェックボックスによる複数選択と一括付け替えを付ける。 「間違ったグループに入った行をまとめて移す」が1操作で終わる。
- 保存後は一覧と集計ページの両方のキャッシュを無効化する。
テスト(必ず書かせる)
groupingKey / pickDisplayName は純粋関数なのでユニットテストが安い。最低限これだけ通す。
A経営WEEK/A経営week/A経営weekが同じキーになる- 前後の空白・連続空白・全角スペースを吸収する
null/undefined/ 空文字 / 空白のみ が空キーになる- 別物のグループを誤って統合しない
- 代表名は件数優先、同数なら新しい方(入力配列を破壊しない)
- 1件 + 8件 が 9件 に統合される
- 候補取得が1000件の上限を越えて2ページ目まで読む
- 候補取得の失敗が空リストとして握り潰されない
さらに、本番の実データで一度再現する。 本番と同じ集計関数に実データを流し、「どのカードが何件になるか」と 「DB の生値が両方残っていること」を目で確認してからデプロイ完了と言う。 ユニットテストが通っただけでは「実データに想定外の表記がない」ことの証明にならない。
完了条件
- 割れていたグループが1枚に統合され、件数が合計値になっている
- そのカードをクリックした先の一覧が0件ではない(落とし穴1の確認)
- DB の生値が両方とも残っている(書き換えていない)
- 詳細画面から別グループへ付け替えでき、候補が既存名から選べる
- 一覧から複数選択して一括付け替えできる
- ユニットテストが通り、本番実データでの再現結果を目視した
- 型チェック・ビルドが通る(変更前のエラー数と比較し、新規増加0)
よくある質問
+「一覧が表記ゆれで割れるのを、DBを書き換えずに直す(集計キー正規化+付け替えUI)」とは何ですか?
大小文字・全半角の違いだけで同じグループが複数カードに割れる問題を、DBの生値を保ったまま表示側の正規化キーで統合する。集計だけ直してフィルタを直さず「クリックしたら0件」になる最頻出の落とし穴、ページング・候補取得上限の罠、間違ったグループに入った行を後から移す付け替えUIまで含む。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約14万トークンかかりますが、この巻物を使えば約2.5万トークンで済みます。差し引き約11.5万トークン(API料金換算で約170円)・82%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
Next.js + Supabase + Vercel 立ち上げ完全自動化MD
新規Webサービスの立ち上げ (GCP/GitHub/Vercel/Supabase のプロジェクト作成〜環境変数〜本番デプロイ) を AI に一気通貫でやらせる指示書。人間の作業はログイン1回だけ。
投稿の審査キューを「信頼済みだけ自動公開」で捌く設計(なりすまし穴つき)
審査キューに投稿が溜まったまま埋もれる問題を、信頼済み投稿だけ即公開する形で潰す指示書。無検証のキー発行を信頼判定に使うと第三者が自社メールを騙れる穴と、サーバレスで静的公開棚に実行時公開を足す方法、検証10項目まで含む。
DB型サイトを「一覧だけ会員限定・個別ページは残す」に切り替える指示書(Next.js App Router)
自社DBで集客していたサイトが競合のリスト抜き取りに気づいた時の改修手順。名前が並ぶバルクな一覧だけを会員限定にし、個別ページはtitle/H1とCTAを残す。ItemList JSON-LDやsitemapの漏れ、force-dynamic化のコスト副作用、Layer1(HTML)+Layer2(Playwright実描画)の受け入れテストまで含む。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア