テーマ
Implementation Plan
本タスクは
requirements.mdとdesign.md(特に File Structure Plan / Components and Interfaces / Data Models)を実装直前まで翻案したものである。各タスクは原則 1〜3 時間の作業単位で、design.mdで定義された境界を越えない単一責務に留める。
[ ] 1. プロジェクト依存・設定の追加
[x] 1.1 (P) microCMS SDK を依存に追加
microcms-js-sdkをpackage.jsonのdependenciesに追加し、lockfile を更新する- インストール後に
node_modules/microcms-js-sdkが解決され、TypeScript でimport { createClient } from 'microcms-js-sdk'が型解決できることを確認する - Requirements: 1.5, 1.6, 3.6
- Boundary: package.json
[x] 1.2 (P) Next.js 画像最適化に microCMS 配信ホストを許可
next.config.tsのimages.remotePatternsに microCMS の画像 CDN ホストパターンを追加する- 候補者画像 URL を
next/imageで読み込んだ際に "Invalid src" エラーが発生しないことを確認する - 自前画像配信エンドポイントの再導入は禁止(
app/api/images/[id]/**を作らない) - Requirements: 2.7
- Boundary: next.config.ts
[ ] 2. microCMS コレクション・権限の構築(運用基盤)
[ ] 2.1 候補者コレクション
candidatesを microCMS に構築displayName/region/age/height/occupation/hobbies/certifications/motto/dream/message/displayOrder/imagesを design.md のスキーマ表どおりに作成するimagesは repeater 構造でimage(media)+mime(select: JPEG/PNG/WebP)を持ち、サイズ上限 5 MB を強制する- 公開状態モデル(公開 / 下書き / 停止)を有効化し、運営者ロールに対し物理削除を不可とするロール設定を行う
- microCMS 管理画面でテスト用候補者を 1 件作成し、公開/下書き/停止の各状態で API レスポンスから出現/消失することを目視確認する
- Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6
- Boundary: microCMS candidates collection
- Blocked: 外部 SaaS (microCMS) ダッシュボードでの手動オペレーション。コード実装ではなく運営者による管理画面操作が必要なため autonomous mode では完遂不可。運用担当者が microCMS 管理画面でスキーマ定義・ロール権限・公開状態モデルを設定後にチェックを入れる
[ ] 2.2 票数パッケージコレクション
creditPackagesを microCMS に構築name/credits(1 以上)/priceJpy(0 以上)/displayOrder/isActiveを作成するname/credits/priceJpy/displayOrderを作成後編集不可のフィールド権限に、レコード物理削除を不可に設定する(isActiveのみ更新可)- テスト用パッケージを 1 件作成し、管理画面から
credits/priceJpyを編集しようとするとロールによってブロックされることを確認する - Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.7
- Boundary: microCMS creditPackages collection
- Blocked: 外部 SaaS (microCMS) ダッシュボードでの手動オペレーション。コード実装ではなく運営者による管理画面操作 (スキーマ定義 + フィールド権限 + ロール設定) が必要なため autonomous mode では完遂不可
[ ] 2.3 公開サイト用の読み取り専用 API キーを発行
candidates/creditPackagesの GET のみを許可した API キーを発行し、書き込み権限が無いことを確認する- 発行したキーは
MICROCMS_API_KEY_READONLYとして Vercel 環境変数(および.env.local)に投入する手順を運用ドキュメントに残す - キーで
GET /candidatesが成功し、POST /candidatesが 401/403 で拒否されることを curl で確認する - Requirements: 1.5, 3.6
- Boundary: microCMS API Keys
- Blocked: 外部 SaaS (microCMS) ダッシュボードでの手動オペレーション。API キーの発行・権限設定・環境変数投入は運用担当者の作業。autonomous mode では完遂不可
[ ] 3. Neon Prisma schema と初期マイグレーション
[x] 3.1 Prisma schema を作成
prisma/schema.prismaを生成し、generator client/datasource db (postgresql, env("DATABASE_URL"))を宣言するPaymentStatusenum(PROCESSING/SUCCEEDED/FAILED/REFUNDED)を定義するVote/Purchase/PurchaseItemモデルを design.md の物理データモデル節どおり(カラム型・PK・FK・@updatedAt・onDelete: Restrict/Cascade)に定義する- 越境参照カラム(
Vote.candidateId/Purchase.candidateId/PurchaseItem.creditPackageId)はStringで外部キーを持たせない @@unique([paymentIntentId])/@@unique([purchaseId, creditPackageId])を含む UNIQUE / INDEX を全て付与するprisma validateが成功し、Prisma がCandidate/CandidateImage/CreditPackage/VotingPeriod/AdminUserモデルを保持していないことを確認する- Requirements: 4.1, 4.3, 5.1, 5.2, 5.6, 6.1, 6.3, 6.4
- Boundary: prisma/schema.prisma
[x] 3.2 初期マイグレーションを生成し CHECK 制約を追加
prisma migrate dev --name initで初期マイグレーション SQL を生成する- 同マイグレーション内に
ALTER TABLE purchase_items ADD CONSTRAINT purchase_items_quantity_positive CHECK (quantity >= 1);を追記する - ローカル DB(
docker-composeの postgres:17)にマイグレーションを適用し、\d+ votes/\d+ purchases/\d+ purchase_itemsで全テーブル・制約・インデックスが期待通り存在することを確認する - Requirements: 6.2
- Boundary: prisma/migrations
- Depends: 3.1
[ ] 4. 共有クライアントと期間設定の実装
[x] 4.1 (P) Prisma 共有クライアント
lib/prisma.tsを作成し、@prisma/clientと@prisma/adapter-pgを組み合わせた PrismaClient シングルトンを export する- 開発時 HMR で複数インスタンスが生成されないよう
globalThisキャッシュを採用する - 任意の Server Component / Server Action から
import { prisma } from '@/lib/prisma'で同一インスタンスを取得できることを確認する - Requirements: 4.1, 4.4, 5.1, 5.2, 5.3, 5.5, 6.1, 6.4
- Boundary: lib/prisma.ts
- Depends: 3.2
[x] 4.2 (P) microCMS 共有クライアント
lib/microcms.tsを作成し、createClient({ serviceDomain, apiKey: process.env.MICROCMS_API_KEY_READONLY })で生成した Client を export する- API キー未設定時にモジュールロード時点で明示的にエラーを throw する(読み取り専用キーの未配線を早期検出する)
lib/microcms.ts経由でない直接microcms-js-sdk呼び出しは禁止であることをコメントで明記する- Requirements: 1.5, 3.6
- Boundary: lib/microcms.ts
- Depends: 1.1
[x] 4.3 (P) 投票期間ユーティリティ
lib/voting-period.tsにVotingPeriod型、InvalidVotingPeriodError(cause:MISSING/INVALID_FORMAT/INVERTED_RANGE)、getVotingPeriod()を実装するVOTING_PERIOD_START/VOTING_PERIOD_ENDを ISO 8601(タイムゾーン付き)でパースし、未設定・不正書式・endsAt <= startsAtで対応するInvalidVotingPeriodErrorを throw する- モジュールスコープで結果をメモ化し、同一プロセス内では 2 回目以降の
getVotingPeriod()が再パースを行わないことを確認する process.env.VOTING_PERIOD_*を直接読む経路を本ファイル以外に作らないことをコメントで明記する- Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
- Boundary: lib/voting-period.ts
[ ] 5. CMS サービス層の実装
[x] 5.1 CMS ドメイン型の宣言
features/cms/types.tsにCandidateImageRef/Candidate/CreditPackageを design.md と一致する Readonly 型として定義するmimeは'image/jpeg' | 'image/png' | 'image/webp'のリテラル合併で固定する- 他 spec(
home/candidate-detail/candidate-ranking/voting)がimport type { Candidate } from '@/features/cms/types'で参照できることを確認する - Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2.4, 3.1, 3.2, 3.5
- Boundary: features/cms/types.ts
[x] 5.2 (P) 候補者取得サービス
features/cms/services/candidateService.tsにgetCandidates()/getCandidate(id)を実装する- microCMS から取得した
images配列をdisplayOrder順に保持し、先頭をメイン画像扱いできる順序で返す - 公開済みコンテンツのみが返り、下書き・停止状態のコンテンツが除外されることをテスト用 microCMS で確認する
fetch直接呼び出し or Reactcache()ラップにより、同一 Request 内で同一引数の呼び出しが 1 回の microCMS HTTP に dedupe される(Request Memoization 契約)next.revalidate = 120のキャッシュを設定する- Requirements: 1.5, 1.6, 2.4
- Boundary: features/cms/services/candidateService.ts
- Depends: 4.2, 5.1
[x] 5.3 (P) 票数パッケージ取得サービス
features/cms/services/creditPackageService.tsにgetActiveCreditPackages()(filters: 'isActive[equals]true')とgetCreditPackage(id)(フィルタなし、過去明細解決用)を実装するgetActiveCreditPackages()がisActive=falseのパッケージを返さないこと、getCreditPackage(id)がisActive=falseでも取得できることをテスト用 microCMS で確認する- Request Memoization 契約(同一 Request 内 1 回)と
next.revalidate = 120をcandidateServiceと同じ規約で適用する - Requirements: 3.6, 3.7
- Boundary: features/cms/services/creditPackageService.ts
- Depends: 4.2, 5.1
[ ] 6. 投票関連サービスの実装
[x] 6.1 (P) ニックネーム正規化サービス
features/voting/services/nicknameService.tsにNICKNAME_MAX_LENGTH = 32、InvalidNicknameError(cause:EMPTY/TOO_LONG)、normalizeNickname(raw)を実装する- 正規化手順: Unicode NFKC →
replace(/\s+/g, ' ')で内部空白圧縮 →trim()→ 長さ 1〜32(UTF-16 コードユニット)検証 - 空または最大長超過で
InvalidNicknameErrorを throw する 'use client'を含めず純関数モジュールとして実装し、Client Component / Server Component / Server Action / Webhook ハンドラのいずれからも import 可能であることをコメントで明記する- Requirements: 4.2
- Boundary: features/voting/services/nicknameService.ts
[x] 6.2 (P) 投票集計サービス
features/candidates/services/voteAggregationService.tsにgetVoteCountsByCandidateIds(candidateIds)/getTopVotersByCandidateId(candidateId, limit?)を実装するgetVoteCountsByCandidateIdsはVote INNER JOIN Purchase ON v.purchaseId = p.id WHERE p.status='SUCCEEDED' AND v.candidateId IN (...)で GROUP BY し、Map<candidateId, count>を返すgetTopVotersByCandidateIdは同条件でニックネーム単位に GROUP BY し、ORDER BY voteCount DESC, nickname ASCでlimit(既定 5)件を返すPROCESSING/FAILED/REFUNDEDの Purchase 由来の Vote が集計に含まれないことを単体テストで確認する- 戻り値が
ReadonlyArray<...>/Map<...>型契約に一致することを確認する - Requirements: 4.4, 4.5, 4.6, 4.7, 4.8
- Boundary: features/candidates/services/voteAggregationService.ts
- Depends: 4.1
[ ] 7. 検証
[x] 7.1 (P) ユニットテスト
normalizeNicknameの境界(NFKC、全角空白、複数空白、trim、0 文字、1 文字、32 文字、33 文字)で正規化値およびInvalidNicknameErrorを検証するgetVotingPeriodの未設定 / 不正書式 /endsAt <= startsAtでInvalidVotingPeriodError、正常系で{ startsAt, endsAt }が返ることを検証するvoteAggregationServiceの SUCCEEDED 集計、PROCESSING/FAILED/REFUNDED 除外、応援者ランキングのタイブレーク(voteCount 降順 + nickname 昇順)を検証するvitestでテストが全件 green になる- Requirements: 4.2, 4.4, 4.5, 4.6, 4.7, 7.1, 7.2, 7.4
- Boundary: features/, lib/
- Depends: 4.3, 6.1, 6.2
[x] 7.2 (P) 統合テスト(取引データストア)
- Purchase + PurchaseItem + Vote の同一
$transaction内一括生成が、VotecreateMany失敗時に全てロールバックされることを確認する paymentIntentIdUNIQUE 違反時に冪等に振る舞う(INSERT が失敗しても既存 Purchase が壊れない)ことを確認する(purchaseId, creditPackageId)UNIQUE が同一パッケージの 2 明細を拒否することを確認する- Purchase 削除で PurchaseItem は Cascade 削除、Vote は
onDelete: Restrictで削除がブロックされることを確認する - 払戻時に Purchase.status のみ更新し Vote 件数が不変、集計結果から消えることを確認する
- Requirements: 4.7, 5.1, 5.3, 5.4, 5.5, 6.3, 6.4
- Boundary: prisma/**, features/candidates/services/voteAggregationService.ts
- Depends: 3.2, 6.2
- Purchase + PurchaseItem + Vote の同一
[x] 7.3 スキーマ・マイグレーション整合検証
prisma migrate deployがクリーン DB に適用でき、prisma db pull(Introspect)でschema.prismaとの差分が出ないことを確認する\d+系でインデックス(votes (candidateId)/votes (candidateId, nickname)/votes (purchaseId)/purchases (paymentIntentId) UNIQUE/purchases (candidateId)/purchases (status)/purchase_items (purchaseId, creditPackageId) UNIQUE/purchase_items (purchaseId))が全て存在することを確認する- 集計クエリの
EXPLAINで意図したインデックスが使われていることを確認する - Requirements: 4.1, 4.4, 4.5, 4.6, 5.1, 5.2, 6.1, 6.3
- Boundary: prisma/**
- Depends: 3.2