Skip to content

feat: 重複ボトルの検知を実装する - #73

Merged
hduehgw0 merged 13 commits into
mainfrom
feature/#57/detect-duplicate-bottles
Aug 6, 2026
Merged

feat: 重複ボトルの検知を実装する#73
hduehgw0 merged 13 commits into
mainfrom
feature/#57/detect-duplicate-bottles

Conversation

@hduehgw0

@hduehgw0 hduehgw0 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

関連 issue

resolve #57

やったこと

概要

同じ物を 2 つ作れないようにした。同一性は銘柄名・年数・樽・限定版の 4 つで判定し、正規化した判定キーを列に持って userId との複合一意制約で保証する。登録・編集のどちらで衝突しても 409 を返し、既存のボトルを示すトーストから詳細へ辿れる。

変更点

  • normalizeText / buildIdentityKey を追加(src/lib/bottle-identity.ts)。正規化は NFKC → toLowerCase() → 空白と不可視文字の除去 の順で固定
  • 共有スキーマが namecaskType の区切り文字(U+001F)を弾く
  • BottleidentityKey 列と @@unique([userId, identityKey]) を追加(マイグレーション適用済み)
  • 書き込みヘルパー createBottle / updateBottle を追加。Route Handler から Prisma を直接呼ぶのをやめ、判定キーの設定を 1 か所に集約
  • POST / PATCH が P2002 を捕まえて 409 と衝突相手のボトルを返す
  • 重複時のトースト(shadcn の Sonner を導入)。カード全体が詳細へのリンク
  • onSubmit の契約を Promise<boolean>Promise<"ok" | "duplicate" | "failed"> に変更
  • ADR-0013 を追加

受け入れ条件(セルフチェック)

  • 文字列正規化の関数が独立してある(NFKC 変換 → toLowerCase() → 空白除去。空白は内部も含めて全て)。feat: 傾向ページを追加する #61 がこの関数だけを import できること
  • 判定キーを組み立てる関数が上の正規化を使って 4 項目を連結する
  • 共有スキーマが namecaskType の区切り文字(U+001F)を弾く
  • Bottle に判定キーの列があり、userId との複合一意制約が張られている(別のユーザーは同じボトルを登録できる)
  • 判定キーは create と update の両方で必ず設定される(薄いヘルパーに集約し、Route Handler から Prisma を直接呼ばない)
  • 登録(POST)と編集(PATCH)の両方で重複を検知し、409 と既存ボトルを返す
  • 重複時に既存のボトルを示すトーストが出て、詳細へ遷移できる(docs/ui-mockups/05・06-重複検出.png
  • 単体テスト:正規化関数(表記ゆれ・空欄・大文字小文字)と区切り文字の拒否
  • 結合テスト:POST/PATCH が 409 と既存ボトルを返す/同じ入力が create と update で同じキーになる
  • 判定キーの設計判断が ADR に残っている(ADR-0013)

動作確認

  • pnpm lint が通る
  • pnpm format:check が通る
  • pnpm typecheck が通る
  • pnpm test が通る(71 件)
  • モバイル幅で表示が崩れない

ブラウザで登録・編集の両方から重複を起こし、トーストの表示と詳細への遷移を確認済み。

Issue 要件外の対応(あれば)

  • next-themes を削除した。pnpm shadcn add sonner が入れる sonner.tsxuseTheme() に依存しており、依存パッケージも一緒に入っていた。本アプリはテーマを切り替えないため、プロバイダ無しで呼ぶと既定の "system"=OS 設定に追従してしまう。色は globals.css のトークン変数から取るようにして依存ごと外した
  • ph-card.svgsrc/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.tsxfeat: 下部タブバーのアプリシェルを追加する #59 が消すため。祖先に居なくても build・typecheck・lint・テストは全て通り toast() が無反応になるだけなので、気づけない壊れ方をする。feat: 下部タブバーのアプリシェルを追加する #59 側にも「触らない」と明記済み
  • **「別のユーザーは同じボトルを登録できる」は自動テストで検証していない。**Route Handler のテストは Prisma をモックしており、一意制約が [userId, identityKey] かどうかを確かめられないため、chore: MVP の完了基準を本番環境で確認する #63 の手動確認へ移した。ただし where: { userId_identityKey: ... } は複合一意からしか型が生成されないので、制約を単独一意に変えると pnpm typecheck が落ちる
  • キーの文字列表現と正規化の適用順を変えるには全行の再計算が要る(列に保存されるため)。順序が効く根拠は BMP 全域を走査して確認済み(120 の符号位置で結果が変わる。例:"ᴬ" は NFKC 先で "a"、小文字先で "A"
  • 重複トーストは 6 秒表示する(sonner の既定は 4 秒)。Material Design がアクション付きスナックバーに示す上限にあたる。無期限も検討したが、内容はユーザーが直前に入力した値なので読解ではなく確認で足り、取り逃しても既存のボトルは一覧から辿れる。閉じるまで残すと画面遷移にも付いてくる
  • 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 段階に割る必要がある。適用済みファイルはチェックサムの都合で書き換えられないため、ここに記録する
  • **本番はこの PR がマージされるまでボトルを登録できない。**マイグレーションを適用した時点で本番のコードが旧版(identityKey を知らない)だったため。マージ=デプロイで解消する。同じことが再発しない構成へ変えるのは chore: 開発 DB と本番 DB を分ける #74

Summary by CodeRabbit

  • New Features

    • Added duplicate-bottle detection when creating or editing bottles.
    • Notifications now link to the existing bottle while preserving submitted form data.
    • Added a bottle placeholder illustration for missing imagery.
    • Improved identity matching across capitalization, spacing, and equivalent text formats.
  • Bug Fixes

    • Prevented duplicate bottle records.
    • Added clearer responses for missing bottles and duplicate edits.
    • Empty values can now be explicitly cleared during updates.
  • Documentation

    • Documented bottle identity handling and duplicate-record behavior.

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mycellar Ready Ready Preview Aug 6, 2026 6:34am

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Bottle duplicate detection

Layer / File(s) Summary
Identity contract and persistence
docs/adr.md, src/lib/bottle-identity.ts, src/lib/schemas/bottle.ts, prisma/schema.prisma, prisma/migrations/...
Bottle identity uses normalized name, age, cask type, and limited status. The database stores the key and enforces uniqueness per user.
Identity-aware bottle service
src/lib/bottles.ts, src/lib/bottles.test.ts
Creation and updates derive identity keys, enforce ownership, and resolve Prisma P2002 errors to duplicate results.
API conflict responses
src/app/api/bottles/route.ts, src/app/api/bottles/[id]/route.ts, src/app/api/bottles/*/route.test.ts
POST and PATCH return 409 with the existing bottle for duplicates. Missing bottles return 404. PATCH requires complete update data.
Duplicate feedback and form outcomes
src/app/bottles/..., src/components/ui/sonner.tsx, src/app/layout.tsx, package.json
Forms preserve input on duplicates and show a Sonner toast with a link to the existing bottle. BottleForm uses explicit submission outcomes.

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
Loading

Possibly related issues

  • hduehgw0/mycellar#70 — Covers duplicate-detection requirements implemented by this change.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation addresses all coding requirements in issue #57, including normalized keys, uniqueness, conflict responses, toast navigation, tests, and ADR documentation.
Out of Scope Changes check ✅ Passed The changes remain related to duplicate detection and its supporting UI, persistence, validation, testing, and documentation requirements.
Title check ✅ Passed タイトルは、重複ボトル検知という変更の主目的を明確かつ簡潔に示しています。
Description check ✅ Passed 説明はテンプレートの各セクションを満たし、変更内容、受け入れ条件、動作確認、制限事項を具体的に記載しています。
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#57/detect-duplicate-bottles

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 828c7ed and efb5f40.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • docs/adr.md
  • package.json
  • prisma/migrations/20260802154139_add_bottle_identity_key/migration.sql
  • prisma/schema.prisma
  • src/app/api/bottles/[id]/route.test.ts
  • src/app/api/bottles/[id]/route.ts
  • src/app/api/bottles/route.test.ts
  • src/app/api/bottles/route.ts
  • src/app/bottles/[id]/edit/edit-bottle-form.tsx
  • src/app/bottles/bottle-form.tsx
  • src/app/bottles/duplicate-bottle-toast.tsx
  • src/app/bottles/new/create-bottle-form.tsx
  • src/app/layout.tsx
  • src/components/illustrations/bottle-placeholder.tsx
  • src/components/ui/sonner.tsx
  • src/lib/bottle-identity.test.ts
  • src/lib/bottle-identity.ts
  • src/lib/bottles.test.ts
  • src/lib/bottles.ts
  • src/lib/schemas/bottle.test.ts
  • src/lib/schemas/bottle.ts

Comment thread src/lib/bottles.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between efb5f40 and 57ac132.

📒 Files selected for processing (6)
  • docs/adr.md
  • src/app/api/bottles/[id]/route.test.ts
  • src/app/api/bottles/[id]/route.ts
  • src/lib/bottles.test.ts
  • src/lib/bottles.ts
  • src/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

Comment thread src/app/api/bottles/[id]/route.ts
@hduehgw0
hduehgw0 merged commit 8f10b9d into main Aug 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 重複ボトルの検知を実装する

1 participant