Claude Codeに設計書を渡して業務管理アプリを段階的に育てた記録
はじめに
建設会社向けの社内業務管理アプリ(Next.js 15 + Prisma + PostgreSQL)を、Claude Codeへの指示書駆動で7フェーズにわたって機能追加し続けた記録です。
アプリの技術スタックは次の通りです。
- フレームワーク: Next.js 15(App Router / standalone出力)
- ORM: Prisma 6.19.2
- DB: PostgreSQL 16(当初 Neon クラウド → 途中で自前VPSに移行)
- 認証: NextAuth.js(JWT)
- UI: Tailwind v4 + shadcn 系コンポーネント
- インフラ: ConoHa VPS + nginx + PM2
「Claude Codeに指示書を貼り付けて実行する」という使い方を軸に、実装・デプロイ・バグ修正・DB移行まで一気通貫でこなしました。個人の感想ですが、指示書の粒度と品質がそのままコードの品質に直結する、という感覚を強く持ちました。
やったこと
フェーズ構成
| フェーズ | 主な内容 |
|---|---|
| 第1弾 | 月次支払表のExcel出力(SheetJS)、TKCコード管理、仕入先別集計 |
| 第2弾 | 西暦化・admin権限制御・CSV一括インポート・exceljs移行・印刷プレビュー |
| 第3弾 | 来期回し・完成処理・仕入入力ガード・二系統レポート・カテゴリインライン変更 |
| 第4弾 | 案件一覧に来期回しチェックボックス列をインライン追加 |
| 第5弾 | 案件の期フィルタを「計上期ベース」に統一 |
| 第6弾 | 決算月変更・経費支払タブ・仕入先分割・仕入インライン編集 |
| 第7弾 | 案件なし仕入対応(projectId NULL許容化) |
指示書の書き方
各フェーズは markdown 形式の指示書をそのまま Claude Code に渡す運用でした。指示書には「実装ステップ(推奨順)」「受け入れ基準」「ファイル一覧(新規/更新)」を必ず入れる書式を使い、Claude Code が独立して判断できる粒度まで落とし込みました。
## 8. 実装ステップ(推奨順)
1. schema 変更 + db push
2. findFiscalYearByDate / getNextFiscalYear ヘルパ作成
3. PATCH /api/projects/[id] 拡張
...
## 9. 受け入れ基準
- [ ] schema に carryOverToNext / recognizedFiscalYearId が追加されている
- [ ] 来期回しONで status=carriedOver、recognizedFiscalYearId=現期+1
exceljs 移行(第2弾)
第1弾で SheetJS(xlsx community版)を使っていましたが、罫線・フォント・塗りつぶしが一切効かないため第2弾で exceljs に切り替えました。
// 合計行の書式例
const totRow = ws.addRow([...]);
totRow.eachCell((cell) => {
cell.font = { bold: true };
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFFFF2CC" } };
cell.border = {
top: { style: "double" },
bottom: { style: "double" },
left: { style: "thin" },
right: { style: "thin" },
};
});
このとき exceljs 固有の罠 にはまりました(後述)。
Prisma の db push 縛り
本番DBは Neon(その後ローカルPG16)で、prisma migrate dev を実行するとドリフト検知でDB全体のリセットを促される危険がありました。そのため全フェーズを通じて prisma db push のみで運用するルールを徹底しています。
# NG: 本番データ消去のリスク
npx prisma migrate dev
# OK: 追加列・新規テーブルのみ非破壊反映
npx prisma db push
npx prisma generate
ハマったポイント
1. exceljs でA列幅だけ反映されない
exceljs のデフォルト列幅が 9 で、幅9を指定した列は <col> タグが省略され Excel 既定値(8.43)で表示されてしまいます。
// NG: A列(幅9)が省略される
ws.columns = [{ width: 9 }, { width: 32 }, ...];
// OK: defaultColWidth を明示して省略列もその幅に揃える
ws.properties.defaultColWidth = 9;
ws.columns = [{ width: 9 }, { width: 32 }, ...];
XMLレベルで defaultColWidth="9" が出力されるかどうかをスクリプトで検証して確認しました。
2. BigInt リテラル 0n がビルドエラーになる
tsconfig の target が ES2019 の環境で BigInt リテラルを書くとコンパイルエラーになります。
// NG: ES2020未満では NG
const val = Number(someSum ?? 0n);
// OK
const val = Number(someSum ?? BigInt(0));
シンプルなミスですが、ビルドログが流れてしまって原因特定に少し時間がかかりました。
3. サーバー上のファイルが別ルートの中身で上書きされていた
rsync で複数の route.ts を転送した際、途中でパスが混線し /api/projects/route.ts に /api/dashboard/route.ts の中身が書き込まれました。
# 症状: /api/projects にアクセスすると dashboard のレスポンスが返る
# ブラウザのAlertで確認
status=200
body={"projectCount":241,"totalSales":...,"monthlySummary":[...]}
d?.data が undefined になりクライアントが Cannot read properties of undefined (reading 'length') でクラッシュ。ハードリロードやシークレットモードでも直らず、最終的にサーバー上のファイルを直接 cat して発覚しました。
教訓: route.ts が多いプロジェクトでは rsync より scp で1ファイルずつ転送する方が安全です。
# 安全: ファイルを明示的に指定
scp -i ~/.ssh/key src/app/api/projects/route.ts \
deploy@server:/tmp/_pjroute.ts
ssh deploy@server "cp /tmp/_pjroute.ts /var/www/app/src/app/api/projects/route.ts"
4. Next.js standalone が localhost へリダイレクトする
Next.js standalone モードでは、middleware が生成するリダイレクト URL に内部ホスト名(localhost:3001)が使われます。nginx の proxy_redirect を通常の文字列置換で設定しても効かず、正規表現版で解消しました。
# NG: 効かない
proxy_redirect http://localhost:3001/ https://example.com/;
# OK: https/http 両方を正規表現で一括置換
proxy_redirect ~^https?://localhost:3001/(.*)$ https://example.com/$1;
5. 侵害されたVPSからのクリーンマイグレーション
CVE-2025-55182(React2Shell)で旧VPSが侵害されたため、新しいVPSへの完全移行が必要になりました。セキュリティ要件を指示書に明記した上で Claude Code に移行作業を実施させました。
## 厳守ルール
- 全て deploy ユーザー(非root)で実施
- コードはソースから再構築(node_modules/.next は持ち込まない)
- NEXTAUTH_SECRET は openssl rand -hex 32 で新規生成
- 旧サーバーへはデプロイしない
DB は Neon から新サーバーのローカル PostgreSQL 16 へ pg_dump → psql でまるごと移行し、admin パスワードを bcrypt で再生成しました。
6. 期フィルタの設計ミスで「来期回し案件が消えた」
案件の期フィルタを orderDate 範囲のみで実装していたため、来期回しフラグ(carryOverToNext=true)を立てた案件が正しい期に表示されませんでした。
修正は recognizedFiscalYearId(計上期の明示指定)を優先し、未指定時だけ orderDate・月別実績・仕入の存在で判定するOR条件に変更しました。
export function getProjectFiscalYearWhere(fy: FiscalYearInfo): Prisma.ProjectWhereInput {
return {
OR: [
{ recognizedFiscalYearId: fy.id }, // 明示指定を最優先
{
recognizedFiscalYearId: null,
OR: [
{ orderDate: { gte: startDate, lt: endExclusive } },
{ monthlyRecords: { some: { yearMonth: { gte: fy.startMonth, lte: fy.endMonth } } } },
{ purchases: { some: { purchaseDate: { gte: startDate, lt: endExclusive } } } },
],
},
],
};
}
ただし最初の実装で orderDate=NULL の案件が190件まるごと消えてしまい、ユーザーから「なくなった」と一報をもらいました。月別実績・仕入データをフォールバックに加えて復旧しました。
7. Bashのcwdドリフトで npm install が親ディレクトリに実行された
Claude Code がシェルのカレントディレクトリを正しく追跡できず、npm install exceljs papaparse が app/ ではなく親ディレクトリで実行されてしまいました。結果として親に package.json と node_modules が作成されました。
発見後、親の誤生成物を削除し、app/ 配下に正しくインストールし直しました。xlsx は scripts/seed.ts が依存していたため削除してしまうとビルドが落ちるとわかり、戻しています。
学び
指示書に「受け入れ基準」を書くと Claude Code が自己検証する
「受け入れ基準」セクションを指示書に含めると、Claude Code は実装後に自分でAPIを叩いたりビルドを通したりして条件をチェックする動きを見せました。指示書を書く側の思考整理にもなります。
prisma 非依存ヘルパに計算ロジックを集約する
支払表の自動計算ロジックを payment-statement-calc.ts という Prisma 依存のないファイルに集約したことで、画面表示・Excel 出力・PDF(仮)で値が必ず一致する構造になりました。同じロジックを複数箇所に書いて食い違いが生じる、という典型的バグを防げます。
prisma db push 縛りで運用するなら migrate status で事前確認する
本番DBが db push 運用なのに migrate dev を実行すると、ドリフト検知でリセットを促されます。prisma migrate status は読み取り専用なので、事前の状態確認に使うと安全です。
Next.js standalone + nginx の落とし穴2点
-
静的ファイルの配信: standalone の
server.jsは_next/static/とpublic/を配信しません。ビルド後に手動でコピーし、nginx のaliasで直接配信する必要があります。 -
リダイレクトの書き換え: middleware 発行のリダイレクト URL に内部ホスト名が入るため、nginx の
proxy_redirect正規表現版で上書きが必要です。
AI駆動開発における「確認ゲート」の重要性
本番DBへのスキーマ変更・旧サーバー削除・TLS発行など、取り消しできない操作の前には必ず「ここでユーザー確認」というゲートを設けました。指示書に 重要ゲートだけ user 確認 と明記しておくと、Claude Code がそこで止まって判断を仰ぐ動きになります。個人の感想ですが、AIに全自動化させるよりも「決定的なポイントだけ人間が判断する」設計の方が、ミスをしたときのダメージコントロールがしやすいと感じました。