diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 65a1b0e..ceca670 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -22,8 +22,9 @@ ## 動作確認 - [ ] `pnpm lint` が通る +- [ ] `pnpm format:check` が通る +- [ ] `pnpm typecheck` が通る - [ ] `pnpm test` が通る -- [ ] `pnpm build` が通る - [ ] モバイル幅で表示が崩れない @@ -19,10 +19,8 @@ - Google ログイン(ユーザーごとにデータを分離) - ボトルの登録・一覧・詳細・編集・削除(必須は銘柄名のみ、他は任意) -- 国・地域 / 年数(NAS 可)/ 樽 / 限定版 / 本数 / メモ の記録 -- コレクションの傾向を簡単なグラフで可視化(国別の本数 など) - -> 次点:写真アップロード・テイスティング記録・AI「今夜の1杯」提案(詳細は `docs/roadmap.md`) +- 産地・地域 / 年数(NAS 可)/ 樽 / 限定版 / 本数 / メモ の記録 +- コレクションの傾向を簡単なグラフで可視化(産地別の本数 など) ## アーキテクチャ @@ -44,37 +42,36 @@ flowchart LR > 「重い分岐があった決定」は ADR(`docs/adr.md`)に記録。下表は各スタックの一言理由。 -| 技術 | 役割 | 選んだ理由(一言) | -| ---------------------------- | ------------- | ---------------------------------------------------------------------------------------------- | -| Next.js(App Router) | フロント+API | フロントと API を 1 つに閉じられる主スタック。現行標準(→ ADR-0002 / ADR-0008) | -| TypeScript | 言語 | 型安全で、フォーム〜API〜DB を一貫した型で繋ぐ | -| Route Handlers | バックエンド | 書き込みAPIを自分で実装。読みは Server Component 直読み(→ ADR-0002) | -| Prisma | ORM | スキーマ駆動で型安全・マイグレーションが一貫(→ ADR-0007) | -| PostgreSQL(Neon) | DB | 定番のリレーショナル DB。サーバーレスで無料枠あり | -| Better Auth(Google) | 認証 | パスワードを保持せず安全。父も使える。auth.js のメンテナンスモード化を受け再選定(→ ADR-0010) | -| Tailwind CSS | スタイル | モバイルファーストを高速に書ける | -| shadcn/ui | UI 部品 | アクセシブルな部品を「自分のコード」として持てる | -| react-hook-form + zod | フォーム/検証 | フォーム管理と型安全なバリデーション | -| Recharts | 可視化 | React と相性が良く、傾向グラフを手早く | -| Vercel | デプロイ | Next.js に最適。push で前後まとめてデプロイ | -| ESLint + Prettier | 規約 | コーディング規約をツールで強制(文書化しない) | -| Playwright + GitHub Actions | テスト/CI | 主要フローの E2E と継続的インテグレーション | -| UploadThing / Vercel Blob | 画像(次点) | 写真保存をマネージドで軽く(→ ADR-0005) | +| 技術 | 役割 | 選んだ理由(選んだ理由が弱い)(一言) | +| ----------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Next.js(App Router) | フロント+API | 現状、フロントエンド領域で最も使用されているフレームワーク。フロントとAPIを1つに閉じられるのも魅力。現行標準(→ ADR-0002 / ADR-0008) | +| TypeScript | 言語 | 型安全で、フォーム〜API〜DB を一貫した型で繋ぐ | +| Route Handlers | バックエンド | 書き込みAPIを自分で理解して実装できる点が魅力。読みは Server Component 直読み(→ ADR-0002) | +| Prisma | ORM | スキーマ駆動で型安全・マイグレーションが一貫しており、`schema.prisma`がそのまま正本となり宣言的なのもいい(→ ADR-0007) | +| PostgreSQL(Neon) | DB | 定番のリレーショナル DB。サーバーレスで無料枠あり | +| Better Auth          | 認証 | auth.jsの非推奨化やメンテナンスモード化を受け再選定(→ ADR-0010) | +| Google OAuth | 認証 | パスワードを保持せず安全!父でもGoogleアカウントを持っており、めんどくさがりな父が登録を一瞬で行える。(→ ADR-0010) | +| Tailwind CSS | スタイル | モバイルファーストを高速に書ける | +| shadcn/ui | UI 部品 | アクセシブルな部品を「自分のコード」として持てる | +| react-hook-form + zod | フォーム/検証 | フォーム管理と型安全なバリデーション | +| Vercel | デプロイ | 小規模なプロダクトに最適。手軽にデプロイでき、各PR毎に確認ができるpreview環境や環境毎に分けられるDBが魅力 | +| ESLint + Prettier | 規約 | コーディング規約をツールで強制 | +| Playwright/Vitest + GitHub Actions | テスト/CI | ? | +| UploadThing / Vercel Blob | 画像 | 写真保存をマネージドで軽く(→ ADR-0005) | ## ドキュメント -| ファイル | 内容 | -| ---------------------- | --------------------------------------- | -| `docs/requirements.md` | 要件概要(目的・スコープ・方針) | -| `docs/user-stories.md` | ユーザーストーリー+受け入れ条件 | -| `docs/data-model.md` | データモデル(ER・Prisma スキーマ下地) | -| `docs/roadmap.md` | ロードマップ+マイルストーン | -| `docs/adr.md` | 意思決定記録(なぜその選択をしたか) | -| `CONTRIBUTING.md` | 開発フロー・コードスタイル・PR | +| ファイル | 内容 | +| ---------------------- | -------------------------------------------- | +| `docs/requirements.md` | 要件概要(目的・スコープ・方針) | +| `docs/data-model.md` | データモデルに関するコンテクスト(メタデータ) | +| `docs/roadmap.md` | ロードマップ+マイルストーン | +| `docs/adr.md` | 意思決定記録(なぜその選択をしたか) | +| `CONTRIBUTING.md` | 開発フロー・コードスタイル・PR | -## セットアップ + - +## セットアップ ```bash # 1. 依存をインストール @@ -100,8 +97,4 @@ pnpm dev > - ローカル:`http://localhost:3000/api/auth/callback/google` > - 本番:`{BETTER_AUTH_URL}/api/auth/callback/google`(例:`https://<本番ドメイン>/api/auth/callback/google`) -## ステータス - -開発中(MVP)。進捗は GitHub Issues / Projects を参照。 - diff --git a/docs/adr.md b/docs/adr.md index 3b0a4aa..cb0a3f1 100644 --- a/docs/adr.md +++ b/docs/adr.md @@ -3,6 +3,10 @@ > ADR = Architecture Decision Record。「なぜその選択をしたか」を残す記録。 > 目的は2つ:将来の自分が経緯を追えること、面接で「なぜ?」に答えられること。 > 形式は軽量(文脈/決定/検討した代替案/結果)。各エントリは将来 `docs/adr/000N-*.md` に分割してもよい。 +> +> **ADRは簡潔にわかりやすく記述する。長いADRは読まれず、記録の目的を果たさない。** 分量は判断の重さに見合わせる。 +> +> **記録は当時の判断であって、現在の前提ではない。** 要件と噛み合わないと感じたら、記録に合わせて要件を削らず、記録の側を疑い、開発者に提示する。決定が変わったら書き換えず、新しい ADR で置き換える(例:ADR-0003 → ADR-0010)。 --- @@ -36,21 +40,21 @@ - マジックリンク … メール送信の外部依存が出る。 - **結果**:パスワードを保持せず安全。父も使え、実装も軽い。Google への依存はあるが個人利用では問題にならない。 -## ADR-0004:データモデルはフラット1テーブル(正規化しない) +## ADR-0004:正規化を先取りしない - **ステータス**:採用(2026-07-02) - **文脈**:個人利用・数百件規模。同一物は「本数」、年数・樽・限定版が違うものは別レコードで表したい。 -- **決定**:`User` 1 ─< `Bottle` のフラット構成。商品マスタと所有を分離しない。 -- **検討した代替案**:正規化(商品マスタ+所有の分離)… 複数ユーザーで同じ商品を共有する「共有カタログ」で輝くが、それは対象外の SNS 領域。登録時の名寄せ(曖昧一致)問題も発生する。 -- **結果**:シンプルで実装・保守が軽い。個人規模で正規化を背負うのは YAGNI。将来、共有カタログをやるなら正規化を検討する。 +- **決定**:**正規化は先取りせず、必要になった時点で判断する。** その結果として現時点の構成は `User` 1 ─< `Bottle` で、商品マスタと所有は分離していない。 +- **検討した代替案**:正規化(商品マスタ+所有の分離)… 複数ユーザーで同じ商品を共有する「共有」で輝くが、それは対象外の SNS 領域。登録時の名寄せ(曖昧一致)問題も発生する。 +- **結果**:シンプルで実装・保守が軽い。現時点で正規化を背負うのは YAGNI。将来、機能追加により正規化を検討する。 -## ADR-0005:画像はマネージドストレージを使い、S3 へは移行しない(写真は次点) +## ADR-0005:画像はマネージドストレージを使い、S3 へは移行しない - **ステータス**:採用(2026-07-02) - **文脈**:父が写真(スマホ撮影)を望む。ただし画像は DB に入れられず保存先が要る=唯一の新規依存。 -- **決定**:写真は次点機能。実装時は UploadThing もしくは Vercel Blob を使い、ストレージ処理を薄いラッパーで包む。生の S3 には降りない。 +- **決定**:実装時は UploadThing もしくは Vercel Blob を使い、ストレージ処理を薄いラッパーで包む。生の S3 には降りない。 - **検討した代替案**: - - 生 S3 … 制御は増えるが設定・運用が重く、個人規模では過剰。 + - 生 S3 … 制御は増えるが設定・運用が重く、現時点では過剰。 - 画像 URL 貼り付け … 最軽量だが、スマホで撮る父には使いにくい。 - **結果**:低コストで写真機能を実現できる。薄い抽象化で将来差し替え可能。規模的に S3 は時期尚早。 @@ -58,9 +62,9 @@ - **ステータス**:採用(2026-07-02) - **文脈**:当初は SNS ブログ案もあったが、実ユーザー(父)の課題は「産地・年数・樽・限定版まで含めて所有ボトルを記録し、コレクションの全体像や傾向を眺めて楽しみたい。」ということ。父は残量・購入日を不要と明言し、写真・産地・年数・樽・限定版・本数を重視した。 -- **決定**:個人カタログ管理に集中する。共有/SNS 化、残量・開封管理、購入日は対象外。 +- **決定**:コレクション管理に集中する。共有/SNS 化、残量・開封管理、購入日は対象外。 - **検討した代替案**:SNS 化(複数ユーザー公開)… 製品の正体が変わる別フェーズで、小機能ではない。 -- **結果**:スコープが明確になり、1ヶ月で完成可能。共有は将来のアイスボックス/別フェーズとする。 +- **結果**:スコープが明確になり、1ヶ月で完成可能。共有は将来に別フェーズとする。 ## ADR-0007:ORM は Prisma を採用する @@ -82,22 +86,45 @@ - **前提**:ストレージ(UploadThing / Vercel Blob)は Pages でも動くため、ルーター選定の決め手にはしていない。 - **結果**:序盤に Server / Client コンポーネントの境界やキャッシュの作法を学ぶコストはあるが、開発者は時間が潤沢で App Router の素地もあるため許容範囲。最速で出すことを最優先するなら Pages も妥当だった、という but 書きを残す。 -## ADR-0009:カテゴリ列は持たず、産地(国)から導出する +## ADR-0009:カテゴリ列は持たず、産地から導出する - **ステータス**:採用(2026-07-02) -- **文脈**:当初は `category` enum(スコッチ/バーボン/ジャパニーズ/ニューワールド/その他)を予定していた。しかし category と region(国)はほぼ 1 対 1 で二重管理になるうえ、「ニューワールド」という総称を enum の 1 値として持つと「日本は JAPANESE か NEW_WORLD か」という分類矛盾が構造的に残る(用語集の定義とも衝突していた)。 -- **決定**:`category` 列を廃止し、産地は `region`(国・固定リスト選択式)に一本化する。「ニューワールド」は保存せず、5 大ウイスキーの国(スコットランド/アイルランド/アメリカ/日本/カナダ)のリストに無ければ新興産地としてプログラムで判定・表示する(導出値は保存しない)。地域の粒度は任意の `subRegion`(アイラ/スペイサイド等)で持つ。 +- **文脈**:当初は `category` enum(スコッチ/バーボン/ジャパニーズ/ニューワールド/その他)を予定していた。しかし category と region(産地)はほぼ 1 対 1 で二重管理になるうえ、「ニューワールド」という総称を enum の 1 値として持つと「日本は JAPANESE か NEW_WORLD か」という分類矛盾が構造的に残る(用語集の定義とも衝突していた)。 +- **決定**:`category` 列を廃止し、産地は `region`(固定リスト選択式)に一本化する。「ニューワールド」は保存せず、5 大ウイスキーの産地(スコットランド/アイルランド/アメリカ/日本/カナダ)のリストに無ければニューワールドとしてプログラムで判定・表示する。地域の粒度は任意の `subRegion`(アイラ/スペイサイド等)で持つ。 - **検討した代替案**: - - enum 維持+用語集の修正 … 矛盾の説明は付くが、region との二重管理と「国と総称が同じ enum に混在する」粒度の混在は残る。 + - enum 維持+用語集の修正 … 矛盾の説明は付くが、region との二重管理と「産地と総称が同じ enum に混在する」粒度の混在は残る。 - IRISH を enum に追加 … 分類は綺麗になるが、二重管理の解消にはならない。 -- **結果**:分類矛盾が起こりようがなくなり、入力項目も 1 つ減る(「5 秒登録」の思想にも沿う)。集計(US-7)は region から導出する。region の固定リストの中身と subRegion(地域)までやるかは、父の回答を待って確定する(TODO)。 +- **結果**:分類矛盾が起こりようがなくなり、入力項目も 1 つ減る(「5 秒登録」の思想にも沿う)。 ## ADR-0010:認証ライブラリを Better Auth に変更する(ADR-0003 の更新) - **ステータス**:採用(2026-07-12)。ADR-0003 を置き換え(Google OAuth・パスワード非保持の方針は維持) -- **文脈**:ADR-0003 で auth.js を採用したが、実装着手前の調査で、Auth.js(旧 NextAuth.js)は **2025-09 に Better Auth チームへ移管されメンテナンスモード**(セキュリティパッチのみ・新機能なし)と公式発表済みであることを確認した。公式は新規プロジェクトに Better Auth を推奨。v5 は 2023-10 からベータのままで、主要コントリビューターも 2025-01 に離脱している。認証はアプリの最外殻であり、コードを書き始める前の今が切り替えコスト最小のタイミング。 -- **決定**:認証ライブラリを Better Auth に変更する。Google OAuth・パスワード非保持(ADR-0003 の本質)は維持。DB スキーマ(User・Session・Account・Verification)は Better Auth CLI の生成に従い手設計しない。セッションは Better Auth 標準の DB セッション(必要に応じて cookie キャッシュで DB リードを削減)。ルート保護はページ/データ取得側での検証を正とし、middleware(proxy)は認可の根拠にしない。 +- **文脈**:ADR-0003 で auth.js を採用したが、実装着手前の調査で Auth.js(旧 NextAuth.js)の停滞を確認した。 + - **2025-09 に Better Auth チームへ移管されメンテナンスモード**(セキュリティパッチのみ・新機能なし)と公式発表済み。公式は新規プロジェクトに Better Auth を推奨。 + - v5 は 2023-10 からベータのままで、主要コントリビューターも 2025-01 に離脱。 + - 認証はアプリの最外殻であり、コードを書き始める前の今が切り替えコスト最小のタイミング。 +- **決定**:認証ライブラリを Better Auth に変更する。付随して以下も決めた。 + - **維持するもの**:Google OAuth・パスワード非保持(ADR-0003 の本質)。 + - **DB スキーマ**:User・Session・Account・Verification は Better Auth CLI の生成に従い、手設計しない。 + - **セッション**:Better Auth 標準の DB セッション(必要に応じて cookie キャッシュで DB リードを削減)。 + - **ルート保護**:ページ/データ取得側での検証を正とし、middleware(proxy)は認可の根拠にしない。 - **検討した代替案**: - auth.js 継続 … MVP 期間の実害は無いが、メンテナンスモード公表後に新規採用する説明が立たず、能動的な保守も期待できない。 - Clerk 等のマネージド認証 … 実装量は最小だが外部依存が増え、「認証を自分で実装した」証明が弱い(ADR-0002 の方針と逆行)。 -- **結果**:能動的に保守されるライブラリ(v1.6 系・リリース頻度高)で認証を構築できる。Prisma 公式の Better Auth 導入ガイドが本プロジェクトの構成(Prisma 7・カスタム出力先)と一致しており、公式のレールに乗れる。User モデルは Better Auth の要求スキーマに合わせた追加マイグレーションが必要(実データ 0 件のため低コスト)。v1.x の API 変動リスクはバージョン固定と公式ドキュメント準拠で管理する。 +- **結果**:能動的に保守されるライブラリ(v1.6 系・リリース頻度高)で認証を構築できる。Prisma 公式の Better Auth 導入ガイドが本プロジェクトの構成(Prisma 7・カスタム出力先)と一致しており、公式のレールに乗れる。 + +## ADR-0011:更新は PUT ではなく PATCH にする + +- **ステータス**:採用(2026-07-22) +- **文脈**:更新エンドポイントのメソッドを決める必要があった。PUT は「リソース全体の置換」、PATCH は「部分更新」を表す。将来は一部の項目だけを更新する機能(例:本数だけ増やす)も見込まれる。 +- **決定**:`PATCH /api/bottles/[id]` にする。フォームは全項目を送るが、メソッドは PATCH を採る。決め手は Prisma の意味論と一致すること、そして**将来の部分更新に同じメソッドで対応できる**こと。 +- **検討した代替案**:PUT(全置換)… 未送信項目を既定値/null にリセットする実装が要る。Prisma の `data` は「未指定キーは変更しない」=部分更新の意味論なので噛み合わず、将来の部分更新でも意味論が破れる。GitHub・Stripe 等もリソース更新は PATCH。 +- **結果**:Prisma と HTTP の意味論が一致し、余分な変換が要らない。部分更新の機能はメソッドを変えずに足せる。ただし現状は `bottleSchema`(全項目必須)で再検証するため**部分更新は受け付けず**、通信量も減っていない(許すなら `.partial()` 等が必要)。PATCH は冪等性を保証しないが、本実装は全項目を置くため結果として冪等。 + +## ADR-0012:ユーザーストーリーを廃止する + +- **ステータス**:採用(2026-07-30) +- **文脈**:MVP の切り出しに `docs/user-stories.md`(US-1〜US-7)を使ったが、新機能の起票先が `requirements.md`「5. スコープ」へ移り、更新が止まった。**ストーリー形式では「まだやらない理由」と「見直しのトリガー」が書けない**ため。また、「対象は MVP」という宣言と中身が 4 箇所で食い違ってもいた。 +- **決定**:削除する。**仕様は機能軸(`requirements.md`「5. スコープ」)、状態は GitHub Issues** に置く。他に記述の無かった受け入れ条件 6 件は、該当する機能の項目へ移した。閉じた Issue・PR の `US-N` は当時の記録なので書き換えない。 +- **検討した代替案**:凍結して残す(`CLAUDE.md` から参照され続け、矛盾も残る)/ストーリーだけ残す(1・2 章と重複)。 +- **結果**:仕様の正本が 1 つに集まり、履歴を辿らずに現状を読める。`US-N` の解決先は失うが、全参照が文脈で自己説明されていた(「ボトルを編集する(US-5)」等)。 diff --git a/docs/data-model.md b/docs/data-model.md index b0b3bb9..9755f84 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -1,106 +1,56 @@ # MyCellar データモデル -> フラット構成(1 テーブル)。`User` 1 ─< `Bottle` の 1 対多。 -> 個人利用・数百件規模のため正規化はしない(理由は末尾「設計判断」参照)。 +> **本ドキュメントはモデルスキーマの実体を持たない。** ここが持つのは、データモデルに関するコンテクスト(メタデータ)のみ。 +> モデルスキーマの実体(正本)や関連ファイルのポインタを以下に示す。 --- -## ER 図 - -```mermaid -erDiagram - User ||--o{ Bottle : owns - - User { - string id PK - string name - string email - boolean emailVerified - string image - datetime createdAt - datetime updatedAt - } - - Bottle { - string id PK - string userId FK - string name "必須(銘柄名)" - string region "国(固定リスト選択式)" - string subRegion "地域(アイラ等・任意)" - int age "年数 / null=NAS or 未入力" - string caskType "樽" - boolean isLimited "限定版" - int quantity "本数 / 既定1" - string photoUrl "写真(次点)" - string note "メモ" - datetime createdAt - datetime updatedAt - } -``` - -## 項目仕様:User - -> Better Auth(Google 認証)が管理するため、これらは**自分で設計・実装しない**(→ ADR-0010)。 -> Google ログイン時に名前・メール・画像 URL が渡され、Better Auth が自動で保存する。 -> **このモデル全体が Better Auth の要求スキーマに従う**(CLI で自動生成し、手で設計・削除しない)。`emailVerified` のように自分では使わないフィールドも、要求されるため勝手に削らない。 -> 認証用の `Session`・`Account`・`Verification` テーブルも CLI 生成に従って追加する(#4 で実装)。**フィールドの最終形は実装時の CLI 生成が正**で、下表は現時点の想定。 - -| 項目 | 型 | 必須 | 備考 | -| --------------------- | -------- | ---- | --------------------------------------------------------------------- | -| id | string | ○ | 主キー | -| name | string? | – | Google から取得 | -| email | string? | – | Google から取得(一意) | -| emailVerified | boolean | – | **Better Auth が要求**(Google OAuth では実質未使用だが削らない) | -| image | string? | – | **Google のプロフィール画像 URL**。文字列が入るだけで実装コストは無い | -| createdAt / updatedAt | datetime | ○ | 作成・更新日時(Better Auth が要求) | - -## 項目仕様:Bottle - -| 項目 | 型 | 入力必須 | 既定 | 備考 | -| --------------------- | -------- | --------- | ----- | --------------------------------------------------------------------------------------------------------- | -| id | string | –(自動) | 自動 | 主キー | -| userId | string | –(自動) | — | 所有者(ログインユーザーから設定。User への FK) | -| name | string | **○** | — | 銘柄名。**唯一の必須項目** | -| region | string? | – | — | 国。**固定リスト選択式**(表記ゆれ防止。リストの中身は父と確定:TODO) | -| subRegion | string? | – | — | 地域(アイラ/スペイサイド等)。region が選ばれている前提の任意項目。地域までやるかは父の回答待ち(TODO) | -| age | int? | – | — | 年数。**null は「NAS」と「未入力」の両方を意味する(区別しない)** | -| caskType | string? | – | — | 樽(シェリー、バーボン樽 等) | -| isLimited | boolean | – | false | 限定版フラグ | -| quantity | int | – | 1 | 同一物の所持本数(1 以上) | -| photoUrl | string? | – | — | 写真 URL。**次点機能で使用**(現行スキーマには未追加。写真機能着手時にマイグレーションで追加) | -| note | string? | – | — | メモ | -| createdAt / updatedAt | datetime | –(自動) | 自動 | 作成・更新日時 | - -> **Bottle の 1 行(1 レコード)は、現実の何に対応するか**: -> 父が「これは別の酒だ」と思う 1 種類 = 1 行。まったく同じ物が増えても行は増やさず `quantity` を足す。 -> 年数・樽・限定版のどれかが違えば「別の酒」なので新しい行を作る。 -> (ひとことで:**同じ物が増える → 本数を足す/違う物 → 行を足す**) - -## Prisma スキーマ(正本は実ファイル) - -> 実装済みのため、スキーマの正本はリポジトリの実ファイル。本ドキュメントにコードの複製は置かない(二重管理防止)。本ドキュメントが持つのは「項目仕様」と「設計判断」まで。 - -| 実ファイル | 内容 | -| ---------------------- | ---------------------------------------------------- | -| `prisma/schema.prisma` | モデル定義(正本) | -| `prisma.config.ts` | 接続設定(Prisma 7 形式) | -| `prisma/migrations/` | 適用済みマイグレーション(DB に流れた実 SQL の履歴) | - -**Prisma 7 での主な変更点**(当初この節に置いていた下地は Prisma 6 形式だった): - -- 接続 URL は `schema.prisma` の datasource ではなく **`prisma.config.ts` で指定**する(schema 側は `provider = "postgresql"` のみ) -- **`directUrl` は廃止**。config の `url` は CLI(migrate 等)専用のため **direct(`DATABASE_URL_UNPOOLED`)**を指定し、アプリ実行時は pooled(`DATABASE_URL`)をクライアント初期化コード側で指定する -- generator は `prisma-client`(旧 `prisma-client-js`)で、クライアントは **`src/generated/prisma`** に生成される(git 管理外・`prisma generate` で生成) - -## 設計判断(ADR 候補のメモ) - -- **正規化しない(フラット 1 テーブル)**:商品マスタと所有を分離する別案は、複数ユーザーで同じ商品を共有する「共有カタログ」で輝くが、それは「やらないこと(SNS 化・別フェーズ)」の領域。個人利用・数百件で正規化を背負うのは YAGNI。将来、共有カタログをやるなら正規化を検討する。 -- **カテゴリ列は持たない(国から導出)**:category と region(国)はほぼ 1 対 1 で二重管理になり、「ニューワールド」という総称を enum の 1 値に持つと「日本はどちらか」という分類矛盾が構造的に残る。「ニューワールド」は保存せず、5 大ウイスキーの国リストに無ければ新興産地としてプログラムで判定・表示する(導出値は保存しない)。→ ADR-0009 -- **`region` は国の固定リスト選択式**:表記ゆれを防ぎ、絞り込み(US-3)と集計(US-7)を安定させる。DB 上は文字列とし、選択肢はアプリ側(zod の固定リスト)で管理する(国の追加をマイグレーション無しで可能に)。リストの中身は父と確定する(TODO)。 -- **`subRegion`(地域)は任意**:スコッチの地域(アイラ/スペイサイド等)を記録したいケース用。region(国)が選ばれている前提の任意項目で、地域だけの入力はしない。地域までやるかは父の回答待ち(TODO)。 -- **`onDelete: Cascade`**:User 削除時に紐づく Bottle も削除。 - -## 対象外(このモデルに含めないもの) - -- テイスティング記録・AI 提案(次点)→ 必要になったら `TastingNote` 等を別テーブルで追加(フラットなので拡張は容易) -- 地図・歴史・バッジ 等(アイスボックス) +## 関連ファイル + +| 実ファイル | データとして持つもの | +| --------------------------- | --------------------------------------------------------------------------- | +| `prisma/schema.prisma` | モデルスキーマの実体(正本)。**フィールド単位の説明は `///` コメント**にある | +| `src/lib/schemas/bottle.ts` | 入力契約(必須・既定・産地の選択肢)。フォームと Route Handler で共有 | +| `prisma/migrations/` | 適用済みマイグレーション(DB に流れた実 SQL の履歴) | +| `prisma.config.ts` | 接続設定(Prisma 7 形式) | + +## 概要 + +### スキーマは前提ではなく判断 + +現在のモデルスキーマ(`schema.prisma`)は、その時点の要件に対する判断にすぎない。実装前や実装中に要件と構造が噛み合わないと感じたら、**構造に合わせて要件を削らず、構造の側を疑い、開発者に提示する**。 + +### Bottle の 1 行は、現実の何に対応するか + +父が「これは別の酒だ」と思う 1 種類 = 1 行。 + +**同じ物が増える → 行は増やさず `quantity` を足す。/ 違う物 → 行を足す。** + +### 「同じ物」の定義 + +**銘柄名・年数・樽・限定版**の 4 つが一致すれば同じ物とみなす。産地・地域・メモ・本数は判定に入れない。 + +| 入れない項目 | 理由 | +| ------------ | -------------------------------------------------------------------------------------------------------------- | +| 産地・地域 | 銘柄名が決まれば一意に決まるため(「山崎」は常に日本)。判定に入れても区別が増えず、入力漏れで別行に割れるだけ | +| メモ | 任意のボトル(種類)についての記録。同じ物が 2 本あってもメモは 1 つでよいため | +| 本数 | 判定の**結果**を入れる先であって、判定の材料ではない | + +### 重複をどう防ぐか + +同じ物を 2 回登録できないようにしたい。ただし `name`・`age`・`caskType`・`isLimited` の 4 列に一意制約を張っても効かない。**Postgres は NULL 同士を別の値として扱う**ため、年数も樽も空の「山崎」は何行でも登録できてしまう。 + +そこで 4 項目を**区切り文字でつないだ**文字列(`山崎|12|シェリー|0`)を別の列に持ち、**`userId` との複合で一意制約を張る**。空欄は空文字になるので NULL が消え、比較が効く。 + +区切り文字を省くと、銘柄名「山崎12」+年数なしと、銘柄名「山崎」+12 年が同じ文字列になる。`userId` を外すと、誰かが登録済みのボトルを他の人が登録できなくなる。 + +### Prisma 6 → 7 で変わったこと + +**接続 URL が `schema.prisma` から出た。** datasource に書くのは `provider = "postgresql"` だけ。`directUrl` は廃止され、URL の指定は 2 か所に分かれる。 + +| 用途 | 指定する場所 | 使う URL | +| ------------------- | ------------------------ | --------------------------------- | +| CLI(`migrate` 等) | `prisma.config.ts` | direct(`DATABASE_URL_UNPOOLED`) | +| アプリ実行時 | クライアント初期化コード | pooled(`DATABASE_URL`) | + +**クライアントの生成先が変わった。** generator は `prisma-client`(旧 `prisma-client-js`)になり、出力先は `node_modules` ではなく `src/generated/prisma`。git 管理外なので **clone 直後は `pnpm prisma generate` が要る**。 diff --git a/docs/requirements.md b/docs/requirements.md index c93d7c6..5336bbb 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -1,88 +1,136 @@ # MyCellar 要件概要 -> 父のウイスキーコレクションを管理する、自分専用の在庫・カタログ Web アプリ。 -> 本ドキュメントは全ドキュメントの土台。詳細は各ユーザーストーリー・ADR に委ねる。 +> 本ドキュメントは全ドキュメントの土台。**1 章は製品として目指す姿、7 章が今回の完成条件**(両者は一致しない)。 --- ## 1. 目的 / コンセプト -- **一言で**:父が自宅のウイスキーコレクションを管理する、自分専用の在庫・カタログ Web アプリ。 -- **解く課題**:産地・年数・樽・限定版まで含めて所有ボトルを記録し、コレクションの全体像や傾向を眺めて楽しみたい。 -- **なぜ作るか**:実ユーザー(父)の実課題。ヒアリング済みで、要望に基づいて設計している。 +- **一言で**:父が自宅にある自分専用のウイスキー在庫を管理・コレクションするためのアプリケーション。 +- **解く課題**:父は数百本を所有し、今も買い続けている。**何を・どれだけ・いくら分買っているのかを本人が把握できていない。** 結果、同じ物を買ってしまい、コレクション全体の姿も見えない。 +- **誰の動機か**:父は「持っているものを把握したい」。開発者(息子)は「**買い過ぎに歯止めをかけてほしい**」。動機は違うが、どちらも**可視化**で効く。 +- **どう解くか**:所有ボトルを記録し、**何を・どれだけ・いくら分**を一覧と傾向で見えるようにする。記録する項目は産地・年数・樽・限定版・本数・購入額。 ## 2. ターゲット / 利用環境 -- **主ユーザー**:父(非エンジニア)。将来的に開発者本人も利用しうる。 -- **デバイス**:**スマートフォン中心(モバイルファースト)**。撮影した写真をその場で登録する想定。 +- **主ユーザー**:父(非エンジニア)。 +- **デバイス**:**スマートフォン中心(モバイルファースト)**。撮影した写真をその場で登録する想定。ただしポートフォリオとしては PC ブラウザで閲覧される想定があるためそちらも対応する。 - **言語**:主に日本語 UI。(製品名や一部箇所にその他の言語を使用することもある。) -## 3. スコープ +## 3. 用語定義 -### MVP の芯(必ず作る) +> ドメイン固有の語と、アプリ内で意味を狭めて使う語だけを載せる。 -- **Google 認証**(Better Auth)でユーザーごとにデータを分離 -- **ボトルの CRUD**(登録・一覧・詳細・編集・削除)+所有権制御(自分のデータのみ操作可) -- **記録項目**(リッチだが必須は最小): - - 銘柄名 …**必須** - - 国(固定リスト選択式)/ 地域 / 年数(NAS 可)/ 樽 / 限定版 / 本数(既定 1)/ メモ …**すべて任意** -- **簡単な可視化 1〜2 個**(例:国別の本数、限定版の割合) - -### 次点(芯が動いてから着手) - -- **写真アップロード**(UploadThing もしくは Vercel Blob、薄いラッパー経由で差し替え可能に) -- テイスティング記録(味の評価・レーダーチャート) -- AI「今夜の 1 杯」提案(自分のボトルデータに基づく。**主役にしない**) -- フィルタ・並べ替えの強化(複数軸ファセット) - -### アイスボックス(将来枠・未定 / やるかも) - -> 確信度の低い「やるかも」の置き場。リスト化のみで、今は設計も実装もしない。詳細はロードマップで管理。 -> これらの多くは「所有ボトルの“物語”を豊かにするエンリッチメント層」として一つの方向にまとまる(MVP の正体=個人カタログを壊さず後から育てられる)。 - -- **ラベル写真から AI 自動入力**(写真+AI の合わせ技で入力負担を消す。アイスボックスの最有力!だが、実現可能性は未定!) -- 蒸留所の地図表示(所有ボトルの産地を世界地図に) -- ウイスキー/蒸留所の歴史表示(LLM 生成+出典明示。史実のハルシネーションに注意) -- コレクション実績バッジ(「○か国制覇」等/集計のみで安い) -- ウィッシュリスト(欲しいボトル=二重買い防止の裏返し) -- 読み取り専用の公開コレクションページ(SNS 化せず、1 枚の公開 URL で見せる) -- 店で出会ったウイスキーの地図記録(Google map API) -- **バーコードスキャン**(スピリッツのバーコード DB が貧弱で、父が重視する限定版・樽違いをほぼ判別できない?) -- 価格の外部 API 取得 -- あいまい検索(誤字許容。個人利用では過剰気味) -- 無限スクロール(難易度の割にコスパが低い。ページングは「もっと見る」で足りる) +| 語 | 定義 | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | +| **銘柄名** | 製品ブランドの名前(「山崎」「マッカラン」)。**年数・樽を含まない**。唯一の必須項目 | +| **題名** | 一覧・詳細で見出しとして表示する文字列(「山崎 12年」)。複数項目から組み立てるもので、**保存しない** | +| **同じ物** | 銘柄名・年数・樽・限定版の 4 つが一致すること。産地・地域・メモ・本数は判定に入れない → `data-model.md` | +| **本数** | 同じ物を何本持っているか。**行数ではない** | +| **産地** | スコットランド・日本 など。固定リストからの選択式。傾向ページの「産地別」はこの粒度を指す | +| **地域** | アイラ・スペイサイド など、産地の下位区分。産地が選ばれている前提の任意項目 | +| **樽** | 熟成に使った樽(シェリー・バーボン 等) | +| **限定版** | 通常のラインナップでない販売品。**判定は父の主観に委ねる**(アプリは真偽値だけを持つ) | +| **NAS** | No Age Statement=年数表記のないボトル。本アプリは**年数の空欄を NAS として扱い、未入力と区別しない** | +| **メモ** | その**種類**についての記録。1 本ごとの記録ではない | +| **5 大ウイスキー** | スコッチ(スコットランド)/アイリッシュ(アイルランド)/アメリカン(アメリカ)/カナディアン(カナダ)/ジャパニーズ(日本) | +| **ニューワールド** | 5 大ウイスキー以外の産地。DB には保存せず、産地からプログラムで判定する → ADR-0009 | -### やらないこと(今のところ) +## 4. 前提と制約 -- 残量・開封管理、購入日(**父が不要と明言**) -- **コレクションの相互共有・SNS 化**(複数ユーザー公開=製品の正体が変わる別フェーズ。小機能ではない)これは将来的にやるかもしれないが基盤が整ってから! -- LINE 通知 / 家族アカウント連携 +- **開発体制**:開発者 1 名(本人)。期間は約 4 週間(開始 2026-07-02)→ `roadmap.md` +- **目的が二重**:①父が実際に使うこと ②就職活動のポートフォリオとして提出すること。**判断が割れたら両方を満たす案を採る** +- **実データ規模**:父の所持は **200〜300 本** +- **入力負担**:必須は銘柄名のみ。最悪「名前だけ」で **5 秒で登録**できること +- **外部データに依存しない**:ウイスキーの銘柄マスタや外部 API を前提にしない(無くても成立する設計にする) -## 4. 設計方針(重要な前提) +## 5. スコープ -- **必須は最小(銘柄名のみ)、他は任意+賢いデフォルト**。入力負担を上げない(最悪「名前だけ」で 5 秒登録)。 -- **フィールドは段階導入可**(安く足せる)。芯は最小フィールドで先に通し、項目は育てる。 -- **AI は土台(実データ)の上に乗せる一機能**。主役にしない。 -- **外部依存は最小限**(写真のストレージのみ)。薄く抽象化して差し替え可能にする。 +### MVP -## 5. 非機能要件 - -- **対応環境**:モバイルファーストで作りつつ、PC でも見やすいレスポンシブ対応(ポートフォリオとして PC 閲覧される想定)。最新の Chrome / Safari 想定。 -- **認証**:Google OAuth。パスワードは保持しない。 -- **規模 / 性能**:個人利用(数十〜数百件)。過剰な最適化はしない。 -- **保守性**:スタック内で完結(Next.js / Prisma / Postgres / Vercel)。 - -## 6. 技術スタック(確定) - -| 領域 | 採用 | -| -------------- | --------------------------------------------------- | -| フロント / API | Next.js(App Router)+ Route Handlers / TypeScript | -| DB / ORM | Postgres(Neon)+ Prisma | -| 認証 | Better Auth(Google) | -| デプロイ | Vercel | -| 画像(次点) | UploadThing もしくは Vercel Blob | - -## 7. 用語 - -- **ニューワールド**:5 大ウイスキー(スコッチ/アイリッシュ/アメリカン/ジャパニーズ/カナディアン)以外の新興産地(台湾・インド等)のウイスキー。カテゴリとしては**保存せず**、国からプログラムで判定して表示する(→ ADR-0009)。 -- **NAS**:年数表記のないボトル(Non Age Statement)。 +- **Google 認証**(Better Auth)でユーザーごとにデータを分離。未ログインで保護ページに入るとログイン画面へ誘導し、ログイン後は一覧に着地する +- **ボトルの CRUD**(登録・一覧・詳細・編集・削除)+所有権制御(自分のデータのみ操作可)。削除は確認ダイアログを挟む(誤操作防止) +- **記録項目**(リッチだが必須は最小) + - 銘柄名 …**必須** + - 産地(固定リスト選択式)/ 地域 / 年数(NAS 可)/ 樽 / 限定版 / 本数(既定 1)/ メモ …**すべて任意** +- **重複ボトルの検知**:**銘柄名・年数・樽・限定版**が同じなら同じボトルとみなす(産地・地域・メモ・本数は含めない)。表記ゆれは NFKC 変換と空白除去で吸収し、判定キーを列に持って **`userId` との複合一意制約**で保証する(ユーザーをまたいだ一意にはしない)。登録時に検知したら**エラーを出し、既存のボトルへのリンクを見せる**(本数の加算は詳細画面から行う) +- **傾向(簡単な可視化)**:総本数・銘柄数・産地数のサマリ/産地別の本数/限定版の割合。**銘柄数は重複検知と同じ正規化を掛けてから数える**(表記ゆれで水増ししないため)。産地が未設定のボトルは「未設定」としてグラフに出す(除外しない) +- **UI をモックに合わせる**:完了条件は `docs/ui-mockups/` の 9 枚に一致すること + - 下部タブバー(コレクション・傾向・登録・アカウント)※ナビゲーションは `layout.tsx` に置き、各ページは自分の幅を持たない(後のレスポンシブ対応を layout 1 枚に閉じるため) + - 一覧の 2 カラムカードグリッド。カードに出すのは銘柄名・産地・年数・本数・限定版バッジ + - 0 件のときは空状態を出す + - 詳細は入力済みの全項目を表示し、未入力項目も空のまま見せる(隠さない) + - ダークテーマ確定とアンバー系のアクセント + - 傾向ページ・アカウントページの追加 + +### 採用 + +- **一覧の検索・フィルタ・並べ替え・ページネーション** — **クライアント状態で実装**。全件をサーバで取得してメモリ上で絞る。 +- **PWA化** — `app/manifest.ts` とアイコンのみ。オフライン対応は含まない。 +- **写真アップロード**(Vercel Blob もしくは UploadThing) +- **レスポンシブデザイン** — PC 表示にも対応する。`lg` 以上で下部タブバーをサイドバーに、カードグリッドを 3〜4 列にする。**PC 用の画面を固定で別に作るのではなく、同じ画面を幅で切り替える。** + +### 保留 + +| 項目 | 理由 | 見直しのトリガー | +| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| 分類(モルト/グレーン/ブレンデッド)の項目追加 | 材料不足:一覧の検索・フィルタ・並べ替え・ページネーションや傾向ページの方針をもっと知りたいため。 | 一覧の検索・フィルタ・並べ替え・ページネーションや傾向ページの実装の後 | +| 蒸留所の項目追加 | 材料不足:フィルタ軸にするなら入力補完とセットにしないと表記ゆれが起こる | わからない | +| 参照テーブル+コンボボックス(産地・蒸留所の選択肢をユーザーが足せるようにする) | 材料不足:MVP は固定リストで足りている。リストに無い産地を登録したい場面がまだ実際に出ていない | 固定リストに無い産地・蒸留所を登録したくなったとき・蒸留所の項目追加に着手するとき | +| 重複時に「本数を足しますか?」と確認するダイアログ | 材料不足:MVP はエラー+既存へのリンクで成立する。詳細画面から加算する手数を父が不便と感じるかが未検証 | 父のフィードバック・重複登録が実際に頻発したとき | +| 過去の入力からのオートコンプリート | 材料不足:入力の手間が実際に問題になるかが未検証 | 登録件数が増えて入力が苦になったとき | +| ウイスキーデータセットの調査と整備 | 材料不足:はじめにやるべきじゃなく固まってからやるべきなため | ウイスキーのデータが欲しくなったとき | +| バーコードスキャンからの入力補完 | 前提不足:iOS で動く読み取り手段がない+バーコード索引のデータセットがあるのか不明(むずそうなため!) | ウイスキーデータセットの調査でデータセットが見つかったとき・準備できた時 | +| ラベル写真から AI 自動入力 | 前提不足:**実現可能性が未検証**。写真アップロードが先に要る | 写真が入り、実データで精度を試せるようになったときやウイスキーデータセットの調査でデータセットが見つかったとき・準備できた時 | +| 価格の外部 API 取得 | 前提不足:用途が具体化していない | 使い道が具体化したとき・ウイスキーデータセットの調査でデータセットが見つかったとき・準備できた時 | +| 購入額の記録と月次・年次グラフ | 前提不足:**購入履歴のデータモデル判断が先に要る**(実装が大変そうだから) | 方針が決まったら | +| テイスティングノート | 材料不足:別テーブルが必要そうで実装が広範囲で内容も決まっていないため | 方針が決まった時 | +| ウィッシュリスト | 材料不足:いるか不明なため | 父のフィードバック・必要だと判断した時 | +| コレクション実績バッジ | 材料不足:父が喜ぶか不明。ソシャゲのようで安っぽくなる懸念もある | 父のフィードバック | +| スコッチ/アイリッシュ等への呼称変換・ニューワールド集約 | 材料不足:モックの傾向ページは産地名をそのまま出しており、変換した呼称の置き場がない(ADR-0009 のこの部分は未実装) | 呼称や集約が実際に欲しくなったとき | +| NAS 表示(年数が空のとき「NAS」と出す) | 材料不足:父が年数をどれだけ入力するか未検証。空欄が NAS なのか入力漏れなのかを実データで見たい | 父のフィードバック・実データで空欄の割合が分かったとき | +| 傾向での NAS の割合 | 材料不足:上の NAS 表示を入れてからでないと判断できない | NAS 表示を入れた後 | +| フィルタ・並べ替えの強化(複数軸ファセット) | 前提不足:まず採用の単一軸フィルタを入れてから | 単一軸では足りないと分かったとき | +| あいまい検索 | 材料不足:数百件では部分一致で足りる。Prisma からは raw SQL が要る | 誤字で見つからない事例が実際に出たとき | +| 無限スクロール | 材料不足:難易度の割にインパクトは少なそうなため! | 件数が増えて一覧の描画が重くなったとき・ほぼ完成して時間に余裕がある時 | + +### 除外(作らないと決めた) + +> ここに並ぶのは**意志による決定**だけ。「できなそう」なものは保留にある。 + +| 項目 | 方針 | +| -------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| 残量・開封管理 | **父が不要と明言**。ユーザーの明言に基づく決定 | +| コレクションの相互共有・SNS 化 | 複数ユーザー公開=製品の正体が変わる別フェーズ。小機能ではない | +| 読み取り専用の公開コレクションページ | 認可の穴を増やさない。SNS 化と同じ方向にあたる | +| ウイスキー/蒸留所の歴史表示 | LLM 生成では史実のハルシネーションの責任を負えない | +| 店で出会ったウイスキーの地図記録 | Google Maps API の運用コストを払わない | +| 蒸留所の地図表示 | Google Maps API の運用コストを払わない | +| PWAオフライン対応 | Serwist が webpack 設定を要求する一方 Next 16 は Turbopack 既定。必要性も説明できない | +| アラート(抽選日や限定品のあと何日?) | Web Push+インストール済み PWA が前提で、全項目の中で最も重い | +| PC 表示時にスマホフレームで囲む演出 | **レスポンシブ対応を避けたと読まれ得る**。ポートフォリオでの見せ方は README のスクリーンショットで足りる | + +## 6. 非機能要件 + +> **要らないものは「要件なし」と書く。** 書いていないのと、不要と決めたのは別なので。 + +| 項目 | 要件 | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| **対応環境** | 最新の Chrome / Safari。モバイル幅で崩れないことを必須とし、PC 幅の対応は「採用」で行う | +| **セキュリティ** | 認証は Google OAuth のみでパスワードを保持しない。**読み書きのすべてで、自分の `userId` のデータだけを対象にする** | +| **性能** | 一覧は全件をサーバで取得する方式を採る。父の実データ(200〜300 件)で問題が出ないことを実機で確認する。それ以上の最適化はしない | +| **運用・保守性** | スタック内で完結(Next.js / Prisma / Postgres / Vercel)。**監視・バックアップの仕組みは持たない** | +| **可用性** | **要件なし。** 個人利用のため、Vercel / Neon の無料枠の可用性をそのまま受け入れる | +| **移行性** | **要件なし。** 現行システムが無く、データ移行が発生しない | +| **システム環境** | **要件なし。** サーバーレスで自前の設置環境を持たない | + +## 7. MVP の完了基準 + +すべて満たした時点で MVP 完成とする。 + +- [ ] 画面が `docs/ui-mockups/` の **9 枚と一致している** +- [ ] **本番環境(Vercel)** で父の Google アカウントからログインでき、登録〜編集〜削除まで通る +- [ ] 他人のデータが**見えない・操作できない**(別アカウントで確認) +- [ ] 同じ物を 2 回登録しようとすると**弾かれ、既存のボトルへ辿れる** +- [ ] 実データ規模(200〜300 件)で一覧・傾向ページが壊れない +- [ ] モバイル幅で全画面が崩れない diff --git a/docs/roadmap.md b/docs/roadmap.md index 5d9b306..f8c2de6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,63 +1,22 @@ # MyCellar ロードマップ & マイルストーン -> 方針:計画は**週単位の粗さ**で持つ。アジャイルなのでズレたら調整する前提。日割りで縛らない。 -> 確信度の勾配:**MVP(確定・日付あり)→ 次点(MVP 後・たぶん)→ アイスボックス(やるかも・未定)**。 +> 方針:計画は**フェーズ単位の粗さ**で持つ。アジャイルなのでズレたら調整する前提。日割りで縛らない。 +> 本ドキュメントが持つのは**いつやるか**だけ。**何を作り、何を作らないか**(MVP・採用・保留・除外)は [`requirements.md`](./requirements.md) の「5. スコープ」が正本。 --- ## 全体像 -- 想定期間:約 4 週間(+準備)。休学中で 1 日 ~10h 確保できる前提。 -- **時間が余るほど機能を盛らない**。余剰はバックエンド学習・テスト・仕上げ・ドキュメントに回す。 -- **MVP(動く形)の目標:第 3 週末** / **ポートフォリオとして提出可能:第 4 週末**。 -- 開始日(仮):`2026-07-06`(月)。実際の開始日に合わせて各週の日付を調整する。 +- 想定期間:**当初計画は約 4 週間**(開始 `2026-07-02`/1 日 ~10h 前提・各フェーズ 1 週間)。実績は下表のとおりで、現在フェーズ 3。 +- **MVP(動く形)の目標:フェーズ 3 の完了時** / **ポートフォリオとして提出可能:フェーズ 4 の完了時**。 -## マイルストーン(週次) +## マイルストーン(フェーズ) -| 期間 | マイルストーン | 主な作業 | 対応ストーリー | -| ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | -| Sprint 0(設計) | 計画が固まる | 要件概要・ユーザーストーリー・データモデル・ロードマップ・ADR を Markdown で整備(=本ドキュメント群) | — | -| 準備(〜2 日) | 土台が立つ | `create-next-app`(TS)/ESLint・Prettier・tsconfig strict/`.env.example` 作成/GitHub Issues・Projects 起票/Vercel・Neon 接続/Prisma 初期化 | — | -| 第 1 週 | 認証と DB の土台 | Prisma スキーマ(User・Bottle)+マイグレーション/Better Auth で Google ログイン・ログアウト・保護ルート(セッション戦略・保護方式は現行の公式推奨を確認して決定)/空のボトル一覧ページ(プレースホルダ=ログイン後の着地点)/「自分のデータだけ」の基盤 | US-1 | -| 第 2 週 | ボトル CRUD(芯の本体) | 登録フォーム(zod・必須は銘柄名)/一覧(モバイルファースト・空状態)/詳細/編集/削除(確認・所有権)/Route Handlers(書き込み)+認可・読みは Server Component 直読み/**重要ロジック(zod・認可)の単体・結合テストを実装と併走**(→ `CONTRIBUTING.md` テスト方針) | US-2〜US-6 | -| 第 3 週 | 可視化 + デプロイ | ワイヤーフレーム作成 ↔︎ ドキュメント整理/一覧の再設計(基本フィルタ〔単一軸〕・基本検索〔名前の部分一致〕・Load More ページネーション)/ダッシュボード(国別の本数グラフなど)/モバイル UI 調整・レスポンシブ(PC)対応/本番デプロイ(Vercel)・動作確認/README を実態に合わせて更新(デプロイ URL 反映) | US-3・US-7 | -| 第 4 週 | 品質 + 仕上げ + 父 FB | **E2E(Playwright・主要フロー 1 本)+ テストを細部まで拡充**+ GitHub Actions(CI)※導入時に CLAUDE.md の Commands と PR テンプレへ test を追記/バグ修正・UI 磨き/README 充実(スクショ・デモ GIF)・アーキ図/**父に実際に使ってもらいフィードバック収集**/予備日 | 共通 DoD | - -> **MVP 完了の定義**:第 3 週末に「Google ログイン → ボトルを登録・一覧・編集・削除 → 傾向グラフ」が本番 URL で一通り動く。 - -> **Markdown → 実ツールへの反映(緩い指針)**:文書(README・ADR・要件・ストーリー・データモデル)はリポジトリ作成時に `docs/` へ置けば完了。計画系(ストーリー)は **実装直前** に GitHub Issues 化し Projects に並べる。これ以上は縛らない。 - -## 次点(MVP 後・たぶんやる) - -優先順位順。**父のフィードバックを最有力の根拠にして着手を判断する**。 - -1. 写真アップロード(UploadThing もしくは Vercel Blob) -2. テイスティング記録(味の評価・レーダーチャート) -3. AI「今夜の 1 杯」提案(自分のボトルデータに基づく) -4. フィルタ・並べ替えの強化(複数軸ファセット) - -> **開発インフラ(任意)**:ローカル DB を `docker-compose` でコンテナ化(本番=Neon、ローカル=コンテナ)。MVP には不要で、Docker 学習・環境分離の signal として芯の後に検討。デプロイの Docker 化はしない(Vercel 運用のため不要)。 - -## アイスボックス(やるかも・未定) - -> 日付なし。やるとは限らない置き場。詳細は要件概要を参照。 - -ラベル写真から AI 自動入力 / 蒸留所の地図表示 / 歴史表示 / 実績バッジ / ウィッシュリスト / 読み取り専用の公開ページ / 店で出会ったウイスキーの地図記録 / あいまい検索 / 無限スクロール - -## 参考:ガント(仮・開始日に合わせて調整) - -```mermaid -gantt - title MyCellar MVP ロードマップ(仮) - dateFormat YYYY-MM-DD - axisFormat %m/%d - - section 準備 - 土台構築 :prep, 2026-07-06, 2d - - section MVP - 認証 + DB 土台 :w1, after prep, 6d - ボトル CRUD :w2, after w1, 7d - 可視化 + デプロイ :w3, after w2, 7d - 品質 + 仕上げ + 父FB :w4, after w3, 7d -``` +| フェーズ | マイルストーン | 主な作業 | +| ----------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Sprint 0(設計)
07-02 | 計画が固まる | ・要件概要・ユーザーストーリー・データモデル・ロードマップ・ADR を Markdown で整備(=本ドキュメント群)
・README・CONTRIBUTING・Issue/PR テンプレート | +| 準備
07-02〜07-09(7 日) | 土台が立つ | ・`create-next-app`(TS)+ ESLint・Prettier・tsconfig strict
・`.env.example` 作成
・GitHub Issues・Projects 起票
・Vercel・Neon 接続
・Prisma 初期化 | +| フェーズ 1
07-09〜07-14(6 日) | 認証と DB の土台 | ・Prisma スキーマ(User・Bottle)+マイグレーション
・**認証ライブラリを Better Auth へ再選定(ADR-0010)**
・Google ログイン・ログアウト・保護ルート
・空のボトル一覧ページ(プレースホルダ=ログイン後の着地点)
・「自分のデータだけ」の基盤・環境変数の fail-fast 検証 | +| フェーズ 2
07-15〜07-25(11 日) | ボトル CRUD(MVPの本体) | ・登録フォーム(zod・必須は銘柄名)
・一覧(モバイルファースト・空状態)・詳細・編集・削除(確認・所有権)
・Route Handlers(書き込み)+認可、読みは Server Component 直読み
・**重要ロジック(zod・認可)の単体・結合テストを実装と併走**(→ `CONTRIBUTING.md` テスト方針)
・**Vitest・CI(GitHub Actions)をフェーズ 4 から前倒し** | +| フェーズ 3
07-26〜(進行中) | MVP 完成(本番で動く) | ・ワイヤーフレーム作成 ↔︎ ドキュメント整理
・**重複ボトルの検知**(同一性=銘柄名・年数・樽・限定版/判定キーの列+一意制約/重複時はエラー+既存ボトルへのリンク)※既存の重複行を掃除してからマイグレーションする。UI 作業より先に済ませる(`bottle-form.tsx` を 2 度触らないため)
・デザイントークン確定(ダークテーマ・アンバー系アクセント・見出しフォント)
・アプリシェル:下部タブバーを `layout.tsx` に集約し、ページは幅を持たせない
・既存 4 画面をモックへ(一覧を 2 カラムカードグリッドに・限定版は Switch・本数はステッパー)
・傾向ページを追加(サマリ・産地別の本数・限定版の割合)
・アカウントページを追加
・本番環境動作確認 | +| フェーズ 4
— | 提出可能 + 父 FB | ・PWA化(`app/manifest.ts` +アイコン)
・レスポンシブデザイン(`lg` でサイドバー+グリッド 3〜4 列)
・一覧の検索・フィルタ・並べ替え・ページネーション(クライアント状態)
・写真アップロード(実装先を ADR で決めてから着手。スキーマ変更あり)
・**父に実際に使ってもらいフィードバック収集**
・フィードバック反映・バグ修正・UI 磨き
・**E2E(Playwright・主要フロー 1 本)+テスト拡充**(機能が出揃ってから)
・**開発環境の Docker 化**(`docker compose up` でアプリ+ローカル DB が立ち上がる。本番は Vercel のまま変えない)
・README 整理(スクショ・デモ GIF)・アーキ図
・予備日 | diff --git "a/docs/ui-mockups/01-\343\203\255\343\202\260\343\202\244\343\203\263.png" "b/docs/ui-mockups/01-\343\203\255\343\202\260\343\202\244\343\203\263.png" new file mode 100644 index 0000000..028dce1 Binary files /dev/null and "b/docs/ui-mockups/01-\343\203\255\343\202\260\343\202\244\343\203\263.png" differ diff --git "a/docs/ui-mockups/02-\343\202\263\343\203\254\343\202\257\343\202\267\343\203\247\343\203\263\344\270\200\350\246\247.png" "b/docs/ui-mockups/02-\343\202\263\343\203\254\343\202\257\343\202\267\343\203\247\343\203\263\344\270\200\350\246\247.png" new file mode 100644 index 0000000..dc1a32b Binary files /dev/null and "b/docs/ui-mockups/02-\343\202\263\343\203\254\343\202\257\343\202\267\343\203\247\343\203\263\344\270\200\350\246\247.png" differ diff --git "a/docs/ui-mockups/03-\343\202\263\343\203\254\343\202\257\343\202\267\343\203\247\343\203\263\357\274\210\347\251\272\347\212\266\346\205\213\357\274\211.png" "b/docs/ui-mockups/03-\343\202\263\343\203\254\343\202\257\343\202\267\343\203\247\343\203\263\357\274\210\347\251\272\347\212\266\346\205\213\357\274\211.png" new file mode 100644 index 0000000..673bb20 Binary files /dev/null and "b/docs/ui-mockups/03-\343\202\263\343\203\254\343\202\257\343\202\267\343\203\247\343\203\263\357\274\210\347\251\272\347\212\266\346\205\213\357\274\211.png" differ diff --git "a/docs/ui-mockups/04-\343\203\234\343\203\210\343\203\253\350\251\263\347\264\260.png" "b/docs/ui-mockups/04-\343\203\234\343\203\210\343\203\253\350\251\263\347\264\260.png" new file mode 100644 index 0000000..04af426 Binary files /dev/null and "b/docs/ui-mockups/04-\343\203\234\343\203\210\343\203\253\350\251\263\347\264\260.png" differ diff --git "a/docs/ui-mockups/05-\343\203\234\343\203\210\343\203\253\347\231\273\351\214\262.png" "b/docs/ui-mockups/05-\343\203\234\343\203\210\343\203\253\347\231\273\351\214\262.png" new file mode 100644 index 0000000..168dcbb Binary files /dev/null and "b/docs/ui-mockups/05-\343\203\234\343\203\210\343\203\253\347\231\273\351\214\262.png" differ diff --git "a/docs/ui-mockups/06-\343\203\234\343\203\210\343\203\253\347\267\250\351\233\206.png" "b/docs/ui-mockups/06-\343\203\234\343\203\210\343\203\253\347\267\250\351\233\206.png" new file mode 100644 index 0000000..48053eb Binary files /dev/null and "b/docs/ui-mockups/06-\343\203\234\343\203\210\343\203\253\347\267\250\351\233\206.png" differ diff --git "a/docs/ui-mockups/07-\343\203\234\343\203\210\343\203\253\345\211\212\351\231\244.png" "b/docs/ui-mockups/07-\343\203\234\343\203\210\343\203\253\345\211\212\351\231\244.png" new file mode 100644 index 0000000..038080d Binary files /dev/null and "b/docs/ui-mockups/07-\343\203\234\343\203\210\343\203\253\345\211\212\351\231\244.png" differ diff --git "a/docs/ui-mockups/08-\345\202\276\345\220\221.png" "b/docs/ui-mockups/08-\345\202\276\345\220\221.png" new file mode 100644 index 0000000..206014c Binary files /dev/null and "b/docs/ui-mockups/08-\345\202\276\345\220\221.png" differ diff --git "a/docs/ui-mockups/09-\343\202\242\343\202\253\343\202\246\343\203\263\343\203\210.png" "b/docs/ui-mockups/09-\343\202\242\343\202\253\343\202\246\343\203\263\343\203\210.png" new file mode 100644 index 0000000..239137b Binary files /dev/null and "b/docs/ui-mockups/09-\343\202\242\343\202\253\343\202\246\343\203\263\343\203\210.png" differ diff --git a/docs/ui-mockups/assets/bottle-amber.svg b/docs/ui-mockups/assets/bottle-amber.svg new file mode 100644 index 0000000..daca1de --- /dev/null +++ b/docs/ui-mockups/assets/bottle-amber.svg @@ -0,0 +1,9 @@ + + diff --git a/docs/ui-mockups/assets/ph-card.svg b/docs/ui-mockups/assets/ph-card.svg new file mode 100644 index 0000000..9ec5b0c --- /dev/null +++ b/docs/ui-mockups/assets/ph-card.svg @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/docs/ui-mockups/assets/ph-empty.svg b/docs/ui-mockups/assets/ph-empty.svg new file mode 100644 index 0000000..9254d45 --- /dev/null +++ b/docs/ui-mockups/assets/ph-empty.svg @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/docs/user-stories.md b/docs/user-stories.md deleted file mode 100644 index a4aa10f..0000000 --- a/docs/user-stories.md +++ /dev/null @@ -1,90 +0,0 @@ -# MyCellar ユーザーストーリー & 受け入れ条件 - -> 対象は **MVP の芯のみ**。次点・アイスボックスは対象外。 -> 形式:ストーリー(誰が/何を/なぜ)+ 受け入れ条件(テスト可能な完成の定義 = チェックで埋まれば完成)。 -> 主ユーザー:**父**(非エンジニア・スマホ中心)。 - ---- - -## US-1 ログインする(Google 認証) - -**父として、Google でログインしたい。自分のコレクションを自分だけの状態で扱いたいから。** - -- [ ] 未ログインで保護ページにアクセスすると、ログイン画面に誘導される -- [ ] 「Google でログイン」でログインできる -- [ ] ログイン後、自分のボトル一覧に遷移する -- [ ] ログアウトでき、ログアウト後は保護ページにアクセスできない -- [ ] 自分(自分の userId)のデータしか見えない - -## US-2 ボトルを登録する - -**父として、新しく手に入れたウイスキーを登録したい。何を持っているか記録するため。** - -- [ ] 「銘柄名」だけ入力すれば登録できる(他項目はすべて任意) -- [ ] 任意項目:国(固定リスト選択式)/ 地域 / 年数(空欄=NAS 可)/ 樽 / 限定版(フラグ)/ 本数 / メモ -- [ ] 本数は既定値 1、1 以上の整数 -- [ ] 銘柄名が未入力なら送信できず、分かるエラーが出る(zod バリデーション) -- [ ] 登録後、一覧に反映され、自分のデータとして保存される -- [ ] 同じ銘柄でも年数・樽・限定版が違うものは別レコードとして登録できる(同一物の重複本数は「本数」で表す) - -## US-3 コレクションを一覧で見る/探す - -**父として、持っているボトルを一覧で眺め、絞り込みたい。同じものを買わないため・全体像を把握するため。** - -- [ ] 自分のボトルが一覧で表示される(銘柄名・国・地域・年数・本数・限定版バッジ) -- [ ] スマホ画面で崩れず見やすい(モバイルファースト) -- [ ] 0 件のとき「まだ登録がありません」などの空状態が表示される -- [ ] 国で絞り込みできる -- [ ] 名前で検索できる -- [ ] 「もっと見る」で追加読み込みできる - -## US-4 ボトルの詳細を見る - -**父として、1 本の詳細を見たい。記録した情報を確認するため。** - -- [ ] 一覧から 1 本を選ぶと詳細が見られる -- [ ] 入力済みの全項目が表示される(未入力項目は省略 or 空表示) -- [ ] 詳細から編集・削除に進める - -## US-5 ボトルを編集する - -**父として、登録済みの情報を直したい。後から産地や樽を書き足す/訂正するため。** - -- [ ] 詳細(または一覧)から編集フォームを開ける -- [ ] 既存の値が入った状態でフォームが開く -- [ ] 保存すると内容が更新され、詳細/一覧に反映される -- [ ] 自分のボトルだけ編集できる(所有権制御) - -## US-6 ボトルを削除する - -**父として、手放したボトルを削除したい。一覧を正確に保つため。** - -- [ ] 詳細から削除できる -- [ ] 削除前に確認ダイアログが出る(誤操作防止) -- [ ] 削除後、一覧から消える -- [ ] 自分のボトルだけ削除できる(所有権制御) - -## US-7 コレクションの傾向を見る(簡単な可視化) - -**父として、自分のコレクションの傾向をグラフで眺めたい。集めている実感や偏りを知るため(自己満)。** - -- [ ] 国別の本数が 1 つのグラフで見られる(**本数= quantity の合計**。レコード数ではない) -- [ ] 5 大ウイスキーの国以外は「ニューワールド」としてまとめて表示される(国からプログラムで判定し、DB には保存しない → ADR-0009) -- [ ] 国が未設定のボトルは「未設定」として表示される(グラフから除外しない) -- [ ] (任意でもう 1 つ:限定版の割合 など) -- [ ] データ 0 件でも壊れない -- [ ] スマホで見やすい - ---- - -## 共通の完成条件(Definition of Done 補足) - -- すべての画面がモバイルファーストで崩れない -- 常に「自分のデータのみ」表示・操作できる(認可) -- 入力は銘柄名のみ必須、他は任意+賢いデフォルト -- エラー時は何が問題か分かるフィードバックを出す - -## このドキュメントの対象外 - -- 写真アップロード(次点)/テイスティング記録/AI 提案(次点) -- 地図・歴史・バッジ・ウィッシュリスト・公開ページ 等(アイスボックス) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7b7c82a..622ea05 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,33 +12,48 @@ datasource db { provider = "postgresql" } +/// アプリの利用者。Better Auth の要求スキーマに従う(better-auth CLI 生成・手設計しない → ADR-0010)。 model User { id String @id @default(cuid()) + /// Google から取得。 name String? + /// Google から取得。 email String? @unique + /// Better Auth が要求。Google OAuth では実質未使用だが削らない。 emailVerified Boolean @default(false) + /// Google のプロフィール画像 URL。 image String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt bottles Bottle[] sessions Session[] accounts Account[] - // Better Auth の要求スキーマに従う(better-auth CLI 生成・手設計しない → ADR-0010)。 @@map("user") } +/// 所有しているウイスキー 1 種類。行の粒度の定義は docs/data-model.md を参照。 model Bottle { id String @id @default(cuid()) + /// 所有者。書き込み時はセッションから設定し、リクエストボディの値は使わない。 userId String + /// User を削除すると、そのユーザーのボトルも削除される。 user User @relation(fields: [userId], references: [id], onDelete: Cascade) - name String // 銘柄名(必須) - region String? // 国(固定リスト選択式。選択肢は zod 側で管理) - subRegion String? // 地域(アイラ等・任意) - age Int? // 年数(null = NAS または未入力) - caskType String? // 樽 + /// 銘柄名。唯一の必須項目。 + name String + /// 産地。固定リスト選択式(表記ゆれ防止)。選択肢は zod の REGIONS で管理し、追加にマイグレーションは不要。 + region String? + /// 地域(アイラ/スペイサイド等)。region が選ばれている前提の任意項目で、地域だけの入力はしない。 + subRegion String? + /// 年数。空欄は NAS(年数表記なし)として扱う。未入力と NAS は区別しない。 + age Int? + /// 樽(シェリー、バーボン樽 等)。 + caskType String? + /// 限定版フラグ。 isLimited Boolean @default(false) + /// 同一物の所持本数(1 以上)。同じ物が増えたら行は増やさず、ここを足す。 quantity Int @default(1) + /// メモ。この種類についての記録であって、1 本ごとの記録ではない。同一性の判定には含めない。 note String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -47,6 +62,7 @@ model Bottle { @@map("bottle") } +/// Better Auth が要求する認証モデル(手設計しない → ADR-0010)。 model Session { id String @id expiresAt DateTime @@ -63,6 +79,7 @@ model Session { @@map("session") } +/// Better Auth が要求する認証モデル(手設計しない → ADR-0010)。 model Account { id String @id accountId String @@ -83,6 +100,7 @@ model Account { @@map("account") } +/// Better Auth が要求する認証モデル(手設計しない → ADR-0010)。 model Verification { id String @id identifier String diff --git a/src/lib/schemas/bottle.test.ts b/src/lib/schemas/bottle.test.ts index 55670ba..dd8d464 100644 --- a/src/lib/schemas/bottle.test.ts +++ b/src/lib/schemas/bottle.test.ts @@ -27,7 +27,7 @@ describe("bottleSchema", () => { expect(result.success).toBe(false); }); - it("固定リストにない国は通らない", () => { + it("固定リストにない産地は通らない", () => { const result = bottleSchema.safeParse({ name: "山崎", region: "月" }); expect(result.success).toBe(false); }); diff --git a/src/lib/schemas/bottle.ts b/src/lib/schemas/bottle.ts index 4407997..d6aa4c9 100644 --- a/src/lib/schemas/bottle.ts +++ b/src/lib/schemas/bottle.ts @@ -1,16 +1,13 @@ import { z } from "zod"; -// 国の固定リスト(表記ゆれ防止・選択肢はここで一元管理 → docs/data-model.md)。 -// 5 大ウイスキーの国+主要な新興産地。中身は父と確定するまでの暫定(追加はこの配列に足すだけ)。 +// 産地の固定リスト(表記ゆれ防止・選択肢はここで一元管理 → docs/adr.md の ADR-0009)。 +// 5 大ウイスキーの産地。 export const REGIONS = [ "スコットランド", "アイルランド", "アメリカ", "カナダ", "日本", - "台湾", - "インド", - "オーストラリア", ] as const; // 空文字は「未入力」として undefined に正規化する(DB に空文字を残さない)。