feat: 重複ボトルの検知を実装する - #73
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe change adds normalized bottle identity keys, per-user database uniqueness, centralized create/update services, duplicate-aware API responses, and toast feedback that links to existing bottles. ChangesBottle duplicate detection
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant BottleForm
participant BottleAPI
participant BottleService
participant Prisma
participant DuplicateToast
BottleForm->>BottleAPI: submit bottle data
BottleAPI->>BottleService: createBottle or updateBottle
BottleService->>Prisma: write identityKey-constrained record
Prisma-->>BottleService: created record or P2002 conflict
BottleService-->>BottleAPI: created, notFound, or duplicate result
BottleAPI-->>BottleForm: 201, 404, or 409 response
BottleForm->>DuplicateToast: show existing bottle on 409
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@prisma/migrations/20260802154139_add_bottle_identity_key/migration.sql`:
- Around line 2-5:
既存のbottle行に対応できる段階的なマイグレーションへ変更してください。まずidentityKeyをnullableで追加し、buildIdentityKeyと同じ正規化ロジックで既存行をバックフィルして重複を解消した後、NOT
NULLへ変更し、最後にuserIdとidentityKeyの一意インデックスを作成してください。
In `@src/lib/bottles.ts`:
- Around line 51-64: src/lib/bottles.ts:51-64 の updateBottle で、PATCH の省略項目を
undefined のまま Prisma に渡さないよう既存ボトルを基に完全な更新状態を構成し、その保存値から buildIdentityKey を生成して
data に設定してください。src/lib/schemas/bottle.ts:39-65 は入力項目の省略を許容する定義として変更不要です。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 87c03e5e-fec5-48df-b881-99a763f2f865
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (21)
docs/adr.mdpackage.jsonprisma/migrations/20260802154139_add_bottle_identity_key/migration.sqlprisma/schema.prismasrc/app/api/bottles/[id]/route.test.tssrc/app/api/bottles/[id]/route.tssrc/app/api/bottles/route.test.tssrc/app/api/bottles/route.tssrc/app/bottles/[id]/edit/edit-bottle-form.tsxsrc/app/bottles/bottle-form.tsxsrc/app/bottles/duplicate-bottle-toast.tsxsrc/app/bottles/new/create-bottle-form.tsxsrc/app/layout.tsxsrc/components/illustrations/bottle-placeholder.tsxsrc/components/ui/sonner.tsxsrc/lib/bottle-identity.test.tssrc/lib/bottle-identity.tssrc/lib/bottles.test.tssrc/lib/bottles.tssrc/lib/schemas/bottle.test.tssrc/lib/schemas/bottle.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/api/bottles/`[id]/route.ts:
- Around line 18-19: Update the PATCH validation around bottleUpdateSchema so
omitted quantity and isLimited fields are rejected rather than populated by
their defaults before updateBottle runs. Build the update schema from field
schemas without defaults, or explicitly validate key presence before safeParse,
while preserving null handling required by ADR-0011.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ee9706cd-0e9b-4433-9265-a089efa3dc65
📒 Files selected for processing (6)
docs/adr.mdsrc/app/api/bottles/[id]/route.test.tssrc/app/api/bottles/[id]/route.tssrc/lib/bottles.test.tssrc/lib/bottles.tssrc/lib/schemas/bottle.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/lib/bottles.ts
- src/lib/schemas/bottle.ts
- docs/adr.md
- src/lib/bottles.test.ts
関連 issue
resolve #57
やったこと
概要
同じ物を 2 つ作れないようにした。同一性は銘柄名・年数・樽・限定版の 4 つで判定し、正規化した判定キーを列に持って
userIdとの複合一意制約で保証する。登録・編集のどちらで衝突しても 409 を返し、既存のボトルを示すトーストから詳細へ辿れる。変更点
normalizeText/buildIdentityKeyを追加(src/lib/bottle-identity.ts)。正規化はNFKC → toLowerCase() → 空白と不可視文字の除去の順で固定nameとcaskTypeの区切り文字(U+001F)を弾くBottleにidentityKey列と@@unique([userId, identityKey])を追加(マイグレーション適用済み)createBottle/updateBottleを追加。Route Handler から Prisma を直接呼ぶのをやめ、判定キーの設定を 1 か所に集約onSubmitの契約をPromise<boolean>→Promise<"ok" | "duplicate" | "failed">に変更受け入れ条件(セルフチェック)
NFKC 変換 → toLowerCase() → 空白除去。空白は内部も含めて全て)。feat: 傾向ページを追加する #61 がこの関数だけを import できることnameとcaskTypeの区切り文字(U+001F)を弾くBottleに判定キーの列があり、userIdとの複合一意制約が張られている(別のユーザーは同じボトルを登録できる)docs/ui-mockups/05・06-重複検出.png)動作確認
pnpm lintが通るpnpm format:checkが通るpnpm typecheckが通るpnpm testが通る(71 件)ブラウザで登録・編集の両方から重複を起こし、トーストの表示と詳細への遷移を確認済み。
Issue 要件外の対応(あれば)
next-themesを削除した。pnpm shadcn add sonnerが入れるsonner.tsxがuseTheme()に依存しており、依存パッケージも一緒に入っていた。本アプリはテーマを切り替えないため、プロバイダ無しで呼ぶと既定の"system"=OS 設定に追従してしまう。色はglobals.cssのトークン変数から取るようにして依存ごと外したph-card.svgをsrc/components/illustrations/へ移設した。docs/ui-mockups/assets/は設計資料で import 禁止のため(アセット内のコメントが指定していた実装先)\sは空白しか拾わず NFKC も不可視文字を消さないため、ZWSP・ソフトハイフン・異体字セレクタが残る。銘柄名は Web や PDF からのコピペが主な入力経路で、残すと見た目が同じ 2 行を別物として登録できてしまう=重複検知の目的が達成できない。判定キーは列に保存され、後から変えるには全行の再計算が要るので、行が入る前に決めた。区切りの U+001F はCcなので巻き込まない.trim()もmin(1)も通り抜けるが正規化すると空になり、銘柄名が空の判定キー(と、名前の無いカード)ができてしまうため備考
<Toaster />はルートのsrc/app/layout.tsxに置いた。bottles/layout.tsxは feat: 下部タブバーのアプリシェルを追加する #59 が消すため。祖先に居なくても build・typecheck・lint・テストは全て通りtoast()が無反応になるだけなので、気づけない壊れ方をする。feat: 下部タブバーのアプリシェルを追加する #59 側にも「触らない」と明記済み[userId, identityKey]かどうかを確かめられないため、chore: MVP の完了基準を本番環境で確認する #63 の手動確認へ移した。ただしwhere: { userId_identityKey: ... }は複合一意からしか型が生成されないので、制約を単独一意に変えるとpnpm typecheckが落ちる"ᴬ"は NFKC 先で"a"、小文字先で"A")onSubmitの 3 値は暫定。setError("root.serverError", ...)に寄せれば戻り値ごと不要になる。調査結果は refactor: 共有フォームのサーバエラー表示を RHF の setError("root") に寄せる #46 に記録したmigrate devが非対話環境で実行できないためmigrate diffで SQL を生成しmigrate deployで適用した。migrate diffはテーブルの行数を見ないので、ADD COLUMN ... NOT NULL(DEFAULT なし)という空テーブル専用の SQL になっている。適用時bottleは 0 件だったため成功しているが、バックアップから復元して再実行する場合は NULL 許容で追加 →buildIdentityKeyを呼ぶスクリプトでバックフィル → 重複掃除 →SET NOT NULLと一意インデックス、の 4 段階に割る必要がある。適用済みファイルはチェックサムの都合で書き換えられないため、ここに記録するidentityKeyを知らない)だったため。マージ=デプロイで解消する。同じことが再発しない構成へ変えるのは chore: 開発 DB と本番 DB を分ける #74Summary by CodeRabbit
New Features
Bug Fixes
Documentation