プログラミング初心者がClaude Codeでレンタルキッチン予約システムを作った話【エラー・躓き全記録】
はじめに
プログラミング初心者の私が、Claude Code(VSCode拡張)を使ってレンタルキッチンの予約・決済管理システムをゼロから開発しました。
技術スタックはこちら:
| 項目 | 内容 |
|---|---|
| フレームワーク | Next.js 16(App Router) |
| 言語 | TypeScript |
| ORM | Prisma |
| DB | Neon(PostgreSQL) |
| 認証 | NextAuth.js |
| スタイリング | Tailwind CSS |
| ホスティング | Vercel |
この記事では実際に躓いたエラーと解決策を全部書き残します。同じところでハマっている人の参考になれば!
躓き① package.json にコメントを書いたらサーバーが落ちた
状況
npm run dev で動かしていたサーバーが突然クラッシュしました。
原因
package.json の中に以下のようなコメントを書いてしまっていたのが原因でした。
{
"dependencies": {
// ← この1行をカンマを忘れずに追加
"next": "16.1.6"
}
}
JSONはコメントをサポートしていません。 Claude Codeがファイルを編集する際に補足としてコメントを入れてくれたのですが、そのままにしていたためJSONパースエラーでサーバーが落ちました。
解決策
コメント行を削除してから npm run dev で再起動するだけです。
{
"dependencies": {
"next": "16.1.6"
}
}
教訓: JSONにはコメントが書けません。メモはJSONの外(READMEなど)に書きましょう。
躓き② Failed to open database エラー(Turbopackキャッシュ破損)
状況
サーバー再起動後、以下のエラーが出て起動できなくなりました。
Error: Failed to open database
Caused by:
0: Loading persistence directory failed
1: invalid digit found in string
code: 'GenericFailure'
原因
これはPrismaのDBエラーではなく、Turbopackの内部キャッシュDBが破損していたのが原因でした。サーバーが異常終了した際に .next フォルダ内のキャッシュが壊れていました。
解決策
.next フォルダを削除してから再起動するだけで解決します。
rm -rf .next
npm run dev
教訓: Failed to open database というエラーが出ても、すぐDBの問題と思わないこと。Turbopackのキャッシュが原因のケースがあります。
躓き③ Neonの channel_binding=require でPrismaが接続できない
状況
Neon(クラウドPostgreSQL)の接続文字列をそのままコピーして .env に貼ったところ、ローカルとVercelの両方でDB接続エラーが発生しました。
Error: invalid digit found in string
code: 'GenericFailure'
原因
Neonが2024年頃から接続文字列にデフォルトで channel_binding=require を追加するようになっています。
# Neonがデフォルトで発行する接続文字列
postgresql://user:pass@host/db?sslmode=require&channel_binding=require
channel_binding=require はTLSチャンネルバインディングという中間者攻撃対策の機能ですが、PrismaのRustエンジンがこの認証方式に未対応なため、接続に失敗します。
技術的な詳細
| 認証方式 | 説明 |
|---|---|
SCRAM-SHA-256(通常) |
Prismaが対応。動作する。 |
SCRAM-SHA-256-PLUS(channel_binding) |
TLS情報を認証に組み込む強化版。Prismaのエンジン(Rust製)が未対応。 |
解決策
接続文字列から &channel_binding=require を削除します。
# .env の修正
# 変更前
DATABASE_URL="postgresql://...?sslmode=require&channel_binding=require"
# 変更後
DATABASE_URL="postgresql://...?sslmode=require"
VercelにデプロイしているときはVercel側の環境変数も忘れずに変更してください。
# Vercelの管理画面: Settings → Environment Variables → DATABASE_URLを編集
# 変更後にRedeployが必要
教訓: NeonのDB接続文字列をそのまま使うと Prisma で動かないことがあります。&channel_binding=require を取り除くのを忘れずに。
躓き④ 利用報告してもダッシュボードの消費時間が減らない
状況
カスタマーが /dashboard/reservations/[id]/report で利用報告を送信しても、/dashboard/customer の「ご利用プラン状況」の残り時間が変わらない。
原因
ダッシュボードのプラン消費時間の計算ロジックで、CONFIRMED(予約確定)・IN_USE・REPORTED の全ステータスを合計していたため、予約確定の時点ですでに時間が引かれていました。
// 修正前:CONFIRMED時点で時間が引かれる
status: { in: ["CONFIRMED", "IN_USE", "REPORTED"] }
予約確定(2時間分)→ 残り18時間
利用報告(2時間分)→ 残り18時間 ← 変化なし!
解決策
「利用報告済み(REPORTED)の実績時間のみ」を計上するよう変更しました。
// 修正後:REPORTEDのみ残り時間に反映
status: "REPORTED"
これにより「予約確定 → 時間は変わらない → 利用報告 → 初めて残り時間が減る」という正しい流れになります。
躓き⑤ CプランとA・Bプランの排他制御
要件
Cプランを使い切るまでは、A・BプランでのキッチンがUXに予約できない仕様にしたい。
実装方針
- **APIレベル(サーバーサイド)**でバリデーション → 不正アクセス対策
- **UIレベル(クライアントサイド)**でセレクトボックスをグレーアウト → UX向上
// APIのバリデーション(route.ts)
const hasActiveCPlan = await prisma.reservation.findFirst({
where: {
userId: session.user.id,
plan: "C",
remainingHours: { gt: 0 },
status: { in: ["CONFIRMED", "IN_USE"] }
}
})
if (hasActiveCPlan && (plan === "A" || plan === "B")) {
return NextResponse.json(
{ error: "Cプランを使い切ってからA・Bプランを予約できます" },
{ status: 400 }
)
}
// UIのグレーアウト(KitchenBookingForm.tsx)
<SelectItem value="A" disabled={hasActiveCPlan}>
Aプラン {hasActiveCPlan && "(Cプラン利用中)"}
</SelectItem>
躓き⑥ Navbar のハンバーガーメニューがページ遷移後も残り続ける(ブラックアウト)
状況
カスタマーダッシュボードにアクセスしたところ、画面が半透明のブラックアウト状態になり操作不能になりました。
原因
2つのバグが重なっていました。
-
ブラックアウトの原因:
<Sheet>(Radix UIのハンバーガーメニュー)がページ遷移後もオーバーレイが残り続けてしまう Radix UI の既知の挙動。 -
ヘッダーが事業用になる原因:
isOwnerMenuの判定がuseSession()のクライアント側セッション取得タイミングに依存しており、リロード時に一瞬ずれる。
解決策
pathname が変わるたびにSheetを閉じるよう制御します。
// Navbar.tsx
const pathname = usePathname()
const [sheetOpen, setSheetOpen] = useState(false)
useEffect(() => {
setSheetOpen(false) // パス変更時に強制クローズ
}, [pathname])
return (
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
{/* ... */}
</Sheet>
)
教訓: Radix UIの<Sheet>はページ遷移でも自動クローズしません。usePathname()とuseEffect()で明示的に閉じる処理が必要です。
躓き⑦ Vercel にデプロイ後にデータが表示されない
状況
ローカルでは正常に動いていたのに、Vercelにデプロイするとダッシュボードのデータが一切表示されなくなりました。
原因
ローカルの .env からは channel_binding=require を削除していましたが、Vercel側の環境変数は変更していなかったのが原因でした。
解決策
Vercel管理画面 → Settings → Environment Variables → DATABASE_URL を編集して &channel_binding=require を削除 → Redeploy を実行。
これでデータが正常に表示されました。
実装した主な機能まとめ
証拠写真アップロード(不正防止)
利用報告時に退出写真の添付を必須にしました。
// /api/upload/route.ts
export async function POST(req: Request) {
const formData = await req.formData()
const file = formData.get("file") as File
// バリデーション
if (!file.type.startsWith("image/")) {
return NextResponse.json({ error: "画像ファイルのみ" }, { status: 400 })
}
if (file.size > 10 * 1024 * 1024) {
return NextResponse.json({ error: "10MB以下にしてください" }, { status: 400 })
}
// 保存(本番はS3やCloudinaryへ移行が必要)
const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.jpg`
await writeFile(path.join(process.cwd(), "public/uploads", filename), buffer)
return NextResponse.json({ url: `/uploads/${filename}` })
}
最低保証料金ルール
予定時間の50%未満の利用の場合、50%分は必ず請求されるロジックを実装。
const MINIMUM_CHARGE_RATIO = 0.5
const minimumHours = scheduledHours * MINIMUM_CHARGE_RATIO
let chargedHours = actualUsedHours
if (actualUsedHours < minimumHours) {
chargedHours = minimumHours // 50%を最低保証
minimumChargeApplied = true
}
const newTotalPrice = chargedHours * pricePerHour
今後の課題
- Stripe決済統合(与信確保 → 利用報告後に確定)
- 画像保存をCloudinary or S3に移行
- スマートロックAPIとの連携(利用時間の自動計測)
おわりに
Claude Codeを使った開発は、「なぜそのエラーが起きたのか」まで丁寧に説明してくれるのが想像以上に学習になりました。特に channel_binding=require の仕組みや Radix UI の挙動など、ただコピペするだけでは理解できなかった部分が自然と身についてきた感覚があります。
プログラミング初心者でもAIを上手く使えば実用的なWebアプリが作れる時代になっています。この記事が同じところで詰まっている人の助けになれば嬉しいです!