テーマ
Research & Design Decisions — home
Purpose: home spec の design 書き直し(data-model の 3 系統契約への適合)で行った調査・統合判断・トレードオフを記録する。
Summary
- Feature:
home - Discovery Scope: Extension(既存トップページの実装基盤を 3 系統データアーキテクチャに合わせて再設計)
- Key Findings:
- 旧 home design は Prisma 単独で
candidateService.getAllWithRank()を持ち、/api/images/[id]経由で画像配信していた。新 data-model では候補者・画像とも microCMS、画像は CDN URL を直接利用。 - VotingPeriod は DB シングルトンから Vercel ENV へ移管済み。home/page.tsx は
lib/voting-period.ts経由で取得し、失敗はerror.tsxで表現する。 - 得票集計は
voteAggregationService.getVoteCountsByCandidateIds経由で Purchase.status='SUCCEEDED' を JOIN フィルタする方式。_count.votes(status='ACTIVE')は廃止。
- 旧 home design は Prisma 単独で
Research Log
Topic 1: 旧 home design とのギャップ
- Context: 旧 design は
data-modelがリセット前提だった頃に書かれ、Prisma の_count.votesやVotingPeriodテーブル、/api/images/[id]を前提に組まれていた。 - Sources Consulted:
home/design.md(旧)、data-model/design.md(新)、steering/tech.md、steering/structure.md - Findings:
- 旧
candidateService.getAllWithRank()は Prisma 直叩きで rank 算出も内包していた → 新設計では Server Component (app/page.tsx) が 3 系統合成と rank 付与を担う。 - 旧
votingPeriodService.get()は DB から取得し null を返していた → 新設計はgetVotingPeriod()が throw する。null フォールバックではなくerror.tsxの責務。 - 旧
images[0].id + updatedAt→/api/images/{id}?v=...の組み立て箇所が散在していた → microCMS の CDN URL をnext/imageのsrcに直渡しに置換。
- 旧
- Implications:
app/page.tsxを Server で 3 系統合成 + rank 算出する責務に書き直す必要がある。- 旧 service / API 経路は
Migration Strategyで削除対象として明示。
Topic 2: rank 一元化と楽観更新の整合
- Context: 要件 Feature 4 は「順位の最新化はページ再訪問時とする」と明確に規定しており、投票成功時に rank を変えない実装が必要。
- Sources Consulted:
home/requirements.mdFeature 4、React 19useOptimisticドキュメント - Findings:
- Server 側で
voteCount 降順 + id 昇順タイブレークの rank を確定し、Client に渡す。 useOptimisticの reducer はvoteCountのみ書き換え、rankを据え置く。useCandidateListWithRankは再計算を行わない。
- Server 側で
- Implications:
- フック契約に「rank 再計算禁止」を明文化(
useCandidateListWithRankの Invariants)。 - Testing Strategy で「楽観更新後も rank が不変」を Unit テストで担保。
- フック契約に「rank 再計算禁止」を明文化(
Topic 3: <CandidateCard> の Props 純化
- Context: 旧設計では
<CandidateCard>が voteLabel / voteDisabled を内部で算出していた可能性があり、責務がぼやけていた。 - Sources Consulted: 旧 home design、steering/structure.md(features/components 分離)
- Findings:
- 期間内/外判定は
useVotingPeriodStatusが一元化し、ラベルと disable は<HomePageClient>から props として渡す。 - カードは presentation のみで、ビジネスロジックを持たない。
- 期間内/外判定は
- Implications:
<CandidateCard>の Props をvoteLabel/voteDisabledを明示的に受ける形に固定。- これにより
candidate-detail/ 他画面でカードを再利用する際の振る舞いを揃えやすい(将来拡張余地)。
Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
旧設計: Server が全データを candidateService.getAllWithRank() で取得 | 単一サービスで rank 算出まで完結 | API が単純 | data-model が microCMS / Neon に分かれた今は責務が逆転 | 不採用 |
| 採用: Server Component が 3 系統合成 + rank 確定、サービス層は薄い | 責務分離が明確、data-model の契約に従う | 合成ロジックが Server Component に集まる | Server Component の単体テスト粒度を統合テストで補完 | data-model の境界に沿う |
| Client 側で rank 再計算 | リアルタイム順位反映 | 要件 4.2 に違反、UX 上も視点ブレ | 採用不可 | 不採用 |
Design Decisions
Decision: rank 算出を Server Component (app/page.tsx) に置く
- Context: data-model の
voteAggregationServiceは集計値(Map)のみ返し、rank 付与は呼び出し側責務。home / ranking で rank の意味が異なる(home は id 昇順表示で rank フィールド保持、ranking は voteCount 降順で再ソート)。 - Alternatives Considered:
voteAggregationService側に rank 付与関数を持たせる。- ページごとに Server Component が rank 算出する。
- Selected Approach: 2。home/ranking 双方でアルゴリズムは同じだが、表示順が異なるため呼び出し側で完結させる方がカップリングが低い。
- Rationale: data-model は「集計値の真実」のみ提供。表示順序と rank フィールドの意味は画面 spec の責務。
- Trade-offs: rank 算出ロジックが home/ranking で重複する可能性 → 純関数として共通 util に切り出す余地はあるが、現状は重複コストが低く許容。
- Follow-up: ranking spec の design 書き直し時に「同じ rank 算出関数を共有するか」を検討。
Decision: 投票期間取得失敗は error.tsx のみで表現する
- Context: 旧設計は
votingPeriod === nullを許容しページ内分岐していたが、新lib/voting-period.tsは throw する。 - Alternatives Considered:
- Server Component で catch して
<ErrorPage>を return。 - throw のまま
error.tsxに伝播。
- Server Component で catch して
- Selected Approach: 2。Next.js の error boundary を使う方が pure で、
HomePageClientを誤って呼ばないことを構造で保証できる。 - Rationale: 要件 1.4「ヘッダー・候補者一覧の描画を行わない」を満たす最も確実な手段。
- Trade-offs:
error.tsxの文言が他のエラーと共通になるが、InvalidVotingPeriodErrorのcauseを見て分岐すれば差別化可能。 - Follow-up:
app/error.tsxの整備タスクをkiro-spec-tasks homeで出す。
Decision: 画像は microCMS CDN URL を直接 next/image に渡す
- Context: 旧設計は自前
/api/images/{id}?v=...を経由していた(Prisma で BLOB を持っていた時代の名残)。 - Alternatives Considered:
- 自前 API を残し、microCMS から取得した URL をプロキシ。
next.config.tsのimages.remotePatternsで microCMS ドメインを許可して直渡し。
- Selected Approach: 2。data-model の Out-of-scope に「自前画像配信」が含まれており、再導入は禁止。
- Rationale: パフォーマンス・コスト・設計境界のすべてで優位。
- Trade-offs: microCMS の URL 形式変更に追随する必要があるが、microCMS は安定。
- Follow-up:
next.config.ts修正タスクはdata-model側で先行。home 側は consumer。
Risks & Mitigations
- Server Component のテストカバレッジ: 合成ロジックを Server Component に集めたため、単体テストが書きにくい。→ services をモックして統合テストで担保。
- rank 算出のロジック重複: home / ranking が同じ計算をする。→ 純関数化を ranking spec design で再評価。
- クライアント時計依存の期間判定:
useVotingPeriodStatusがクライアント時計に依存。→ サーバー側 Server Action(votingspec)で必ず再判定する規約を維持。 useOptimisticの API 変更: React 19 の experimental flag が外れている前提。→ 依存バージョン固定 + リリースノート監視。
References
.kiro/specs/home/requirements.md— 機能要件.kiro/specs/data-model/design.md— データ層契約.kiro/steering/structure.md— features / components 責務分離- React 19 useOptimistic — 楽観更新 API
- Next.js 15 Image Optimization (remotePatterns) — microCMS CDN を許可する設定
Gap Analysis — home (2026-05-17 追記)
Purpose: home spec の requirements / design に対し、現行コードベースとの差分を棚卸しし、design phase に持ち込む実装戦略の候補と前提を確定する。
1. Current State Investigation
既存資産の棚卸し
| カテゴリ | 期待される場所 | 現状 | 備考 |
|---|---|---|---|
| Home Server Component | app/page.tsx | プレースホルダー(再構築中です 文字列のみ) | spec で前提とする 3 系統合成は未着手 |
| Home Client Component | app/_components/HomePageClient.tsx | 未作成 | _components ディレクトリ自体が存在しない |
| Layout / Providers | app/layout.tsx / app/providers.tsx | 最小構成のみ(フォント変数 + 空 Provider) | 背景装飾・グローバル状態は未配置 |
| デザイントークン | design-tokens/tokens.css | gold / rose-gold / ivory / charcoal / shadow-card / shadow-modal が定義済み | ui-design.md のパレット要件を充足 |
| グローバル CSS | app/globals.css | shadcn ベース + brand color エイリアス + fade-in-up / card-fade-in / confetti-burst / shimmer keyframes が既に存在 | アニメーション仕様(ui-design.md)とほぼ整合 |
| UI プリミティブ | components/ui/ | button / card / dialog / input / label / sonner / table / textarea のみ | shadcn 基本セットは揃うが home 固有 presentation は皆無 |
| 公開向け presentation | components/public/ | ディレクトリ未作成 | HomeHero / CandidateGrid / CandidateCard / Confetti 等すべてゼロから |
| features 層 | features/** | ディレクトリ未作成 | voting / candidates / cms / payment / voters のすべてが未実装 |
| lib(画面層が依存) | lib/voting-period.ts / lib/microcms.ts / lib/prisma.ts | 未作成(lib/utils.ts のみ存在) | これらは data-model spec の所有物 |
| Prisma スキーマ | prisma/schema.prisma | 未作成 | data-model 所有。home からは不可視 |
next.config.ts | ルート | 設定なし({})。images.remotePatterns 未設定 | microCMS CDN URL を next/image に渡すための前提が満たされない |
| error boundary | app/error.tsx | 未作成 | spec design Flow 3 で必要 |
| Storybook / テスト基盤 | .storybook/ / vitest.config.* / Playwright | 未導入 | tasks.md の 4.1〜4.4 検証タスクの前提が未整備 |
規約・依存方向の確認
steering/structure.mdの 3 層モデル(app → features / app → components)はまだコード上に投影されていない。components/には UI プリミティブのみ存在し、public/区分も未作成のため、新規ファイルはすべて新しいディレクトリの「初投入」となる。app/_componentsのページローカル分割パターン(steering の「現行実装メモ」)は採用予定だが未投入。home tasks 3.1 で初出。- 依存方向の違反は 現時点では存在しない(まだほぼ何もないため)。design.md 通りに進めれば違反は発生しない構造になっている。
統合面・データ取得経路
- microCMS / Neon / Vercel ENV のクライアントインスタンスは未作成 →
home単独では import 不可。data-modelspec 完了が コード上のハードブロッカー。 next/imageを microCMS CDN ホストに向けるためのnext.config.ts > images.remotePatternsは 空。home 実装に進む前にdata-model側で更新される必要がある。
依存パッケージの状況(package.json レベル)
| 必要ライブラリ | 状態 | 備考 |
|---|---|---|
next 16 / react 19 / react-dom 19 | 導入済み | App Router・useOptimistic 利用可能 |
motion 12 | 導入済み | Confetti / hover アニメで利用 |
lucide-react | 導入済み | TrendingUp 等のアイコン |
next/font/google(Cormorant Garamond / Jost) | layout.tsx で読み込み済み | ui-design.md の display / body フォント要件を充足 |
microcms-js-sdk | 未導入 | data-model 側で導入予定。home からは間接利用 |
@prisma/client@^7 / @prisma/adapter-pg@^7 | 導入済み | data-model 側でスキーマ・クライアント生成が必要 |
tailwindcss 4 / tw-animate-css | 導入済み | スタイリング基盤あり |
| Storybook / Vitest / Playwright | 未導入 | tasks 4.x のテスト計画は将来導入分に依存 |
2. Requirements Feasibility Analysis
Requirement-to-Asset Map
| Req | 要件サマリ | 必要なアセット | 現状 | ギャップ種別 |
|---|---|---|---|---|
| 1.1 | Home 表示時にヘッダー + 候補者一覧描画 | Server Component が 3 系統合成 | app/page.tsx プレースホルダ | Missing |
| 1.2 | サイトタイトル「Miss World Japan 2026」/ サブコピー / 投票期間 / ランキング遷移 | <HomeHero> + Intl.DateTimeFormat 整形 | 未作成 | Missing |
| 1.2(投票期間表示) | 期間を data-model 由来値から取得 | lib/voting-period.ts の getVotingPeriod() | 未作成(data-model 所有) | Constraint: data-model 完了待ち |
| 1.3 | 投票期間値は data-model から取得・固定値禁止 | 同上 + テストでハードコード文字列を検知 | テスト基盤未整備 | Missing(検証) |
| 1.4 | 期間取得失敗時はエラー画面 / 一覧描画しない | app/error.tsx + InvalidVotingPeriodError throw | error.tsx 未作成、Error 型は data-model 所有 | Missing |
| 2.1 | 全候補者を一覧表示 | <CandidateGrid> + microCMS から候補者取得 | 未作成 | Missing |
| 2.2 | 各候補者の画像 / 順位 / 得票数(3桁区切り) / 名前 / 詳細導線 | <CandidateCard>(next/image + microCMS CDN URL 直渡し) | 未作成。next.config.ts.images.remotePatterns 未設定 | Missing + Constraint |
| 3.1 | 候補者一覧を ID 昇順表示 | Server で sort((a,b) => a.id.localeCompare(b.id)) | 未実装 | Missing |
| 3.2 | 投票数変動で並び順を変えない | useCandidateListWithRank(useOptimistic reducer は voteCount のみ) | フック未作成 | Missing |
| 4.1 | 現在順位は voteCount 降順で算出 | rankCandidates 純関数(candidate-ranking 所有) | 未作成(別 spec 所有) | Constraint: candidate-ranking 完了待ち |
| 4.2 | 順位最新化はページ再訪時のみ | useCandidateListWithRank の rank 不変保証 | 未実装 | Missing |
| 4.3 | 同得票時は ID 昇順タイブレーク | rankCandidates 内ロジック | 別 spec 所有 | Constraint |
| 5.1 | 候補者詳細への単一操作遷移 | <Link href={/candidate/${id}}> | 遷移先 route 自体も未実装(candidate-detail 所有) | Constraint(リンク先のみ) |
| 共通: 投票導線(2.2 補助) | 購入モーダル + ニックネーム + Confetti | useVoteFlow / useVotingPeriodStatus / <PurchaseFlowContainer>(voting 所有) | 未実装 | Constraint: voting 完了待ち |
| 非機能: LCP / 画像最適化 | 先頭 4 件 priority + sizes | <CandidateCard> 内で実装 | 未実装 | Missing |
| 非機能: メタデータ | app/page.tsx の export const metadata | プレースホルダ実装には未配置 | Missing | |
非機能: テスト(tasks.md 4.x) | Vitest / Testing Library / Playwright | 未導入 | Missing(将来) |
複雑性シグナル
- Algorithmic logic: rank 算出ロジック(降順 + ID タイブレーク) → ただし
candidate-ranking所有なので home 側は import するだけ - Workflow: 投票成功 → 楽観的 voteCount 更新 + Confetti(
useVoteFlow経由) → home 側は配線のみ - External integrations: microCMS / Neon / Stripe(間接) → すべて
data-model/votingを経由するため home スコープには直接出てこない - UI complexity: 高め(背景装飾 + hover アニメ + Confetti + Hero + Card stagger) → ただし
ui-design.mdで確定済み
Research Needed(design phase で詰めるべき項目)
<HomeHero>の投票期間整形フォーマット(ja-JPのdateStyle: 'long'か手書きか)。固定値ハードコード禁止の要件 1.3 に抵触しない実装パターン- 期間表示の ハイドレーション安全性: サーバー(JST)とクライアントの差を Server で完結させて props 化することは決まっているが、ロケール差で年表記が揺れないかを
design phaseで再確認 - Confetti のレイヤリング(z-index)とスクロール位置の関係(
<ScrollToTopButton>との衝突) - 既存
app/globals.cssのアニメーション keyframe (card-fade-in)を Tailwind utility として呼ぶ流儀(animation-nameを直接 style に書くか、@layer utilitiesでクラス化するか) - Server Component の単体テスト粒度(
design.mdRisks にも記載済み)
3. Implementation Approach Options
ベースライン: 現行 app/page.tsx はプレースホルダのみで、再利用すべき既存実装は 皆無。したがって「既存を拡張するか / 新規を作るか」の選択肢は実質的に縮退するが、フォーマルに 3 案を提示する。
Option A: Extend Existing(プレースホルダを段階的に育てる)
- 対象ファイル:
app/page.tsxを直接書き換え、app/layout.tsx/app/providers.tsxの機能拡張で全責務を引き取る - 進め方: 1 ファイルで Server / Client を擬似的に同居させ、後から分割
- 互換性: 後方互換の対象が無いため評価不能(=制約なし)
- トレードオフ:
- ✅ 初動の認知負荷が低い
- ❌
design.mdが明示する Server / Client の責務分離(Boundary Map)に反する - ❌
steering/structure.mdの 3 層モデルを早期に逸脱し、後のvoting/candidate-detailで再分割コストが発生 - ❌
app/page.tsxが肥大化し、<CandidateCard>の再利用が阻害される
評価: 採用非推奨。spec の Architecture Pattern を破壊する。
Option B: Create New Components(spec 通りにゼロから構築)
- 対象作成ファイル(home 単独で新規になるもの):
app/page.tsx(書き換え, Server Component 化)app/error.tsx(新規, Client Component)app/_components/HomePageClient.tsx(新規)features/candidates/types.ts(新規,RankedCandidateの re-export)features/candidates/hooks/useCandidateListWithRank.ts(新規)features/candidates/hooks/useScrollToTop.ts(新規)components/public/HomeHero.tsxcomponents/public/CandidateGrid.tsxcomponents/public/CandidateCard.tsxcomponents/public/Confetti.tsx
- import するが本 spec で作らないファイル(他 spec 所有):
features/cms/services/candidateService.ts(data-model)features/cms/services/creditPackageService.ts(data-model)features/candidates/services/voteAggregationService.ts(data-model)features/candidates/utils/rankCandidates.ts(candidate-ranking)features/voting/hooks/useVoteFlow.ts/useVotingPeriodStatus.ts(voting)features/voting/components/PurchaseFlowContainer.tsx(voting)lib/voting-period.ts/lib/microcms.ts/lib/prisma.ts(data-model)
- 責務境界:
design.mdの Boundary Commitments と完全一致 - トレードオフ:
- ✅ Spec の Architecture / Layered Module Plan に厳密準拠
- ✅ presentation と logic の独立テストが可能(
<CandidateCard>を Storybook 単体で確認可能 =ui-design.mdハンドオフが成立) - ✅ 後続 spec の再利用(
candidate-detail/candidate-rankingが<CandidateCard>派生やフックを共有)に最も近い - ❌ 一度に作成するファイル数が多い(約 10 ファイル + テスト)
- ❌ 他 spec の依存(
data-model/voting/candidate-ranking)が解決するまで一部の import が dangling になる
評価: 採用推奨。design.md / tasks.md がすでに本案で書かれており、整合性が最も高い。
Option C: Hybrid(段階導入)
- 段階分け:
- Phase 1:
data-model完了直後に、<HomeHero>+<CandidateGrid>(投票導線抜き) +useCandidateListWithRankを導入。useVoteFlow等の voting 依存箇所は disabled なボタン + プレースホルダ で表現 - Phase 2:
candidate-ranking完了でrankCandidatesを import、rank 表示を有効化 - Phase 3:
voting完了で投票導線・Confetti・楽観更新を有効化
- Phase 1:
- メリット: 上流 spec の進行と並行して home の UI を可視化できる(デザインレビューの早期実施が可能)
- トレードオフ:
- ✅ デザイナーへの early feedback が可能
- ✅ 各段階で動くものが出る
- ❌ Phase 1 用の暫定 UI を捨てるコストが発生(disabled ボタン → 実ボタンの差し替え)
- ❌
tasks.mdの現行構成(Foundation → Core → Integration → 検証)とフェーズ境界がズレるため、tasks 再生成が必要
評価: 採用検討可。ただし上流 spec(data-model / voting / candidate-ranking)の並行進行が 同時に走る ことが条件。本リポジトリの状況では上流 spec も同程度に未着手なので、Option B の一括着手とほぼ差がない。
4. Implementation Complexity & Risk
| 観点 | 評価 | 一行 justification |
|---|---|---|
| Effort | L (1–2 週) | 新規 ファイル 約 10 + テスト + Storybook 立ち上げ。1 spec 単独としては大きいが、UI / Logic / Hooks の役割が明確で並列化可能 |
| Risk | Medium | 上流 spec(data-model / voting / candidate-ranking)依存があるが、契約は design.md / data-model/design.md で確定済み。未知の技術や複雑な統合は無いが、order-of-implementation を間違えると import が壊れる |
高リスク要因(Mitigation 付き)
- R1. 他 spec の契約変更:
Candidate/RankedCandidate型・voteAggregationServiceのシグネチャが変わると home の全層が再ビルド対象に- Mitigation:
design.mdの Revalidation Triggers に明文化済み。型 import をfeatures/candidates/types.ts経由に集約してチョークポイント化
- Mitigation:
- R2.
next/image+ microCMS CDN 設定不在:next.config.tsのimages.remotePatternsが未設定のまま実装すると<Image>が動かない- Mitigation:
data-model完了マイルストーンにremotePatterns反映 を確認チェックとして含める(本 spec 単独では編集禁止)
- Mitigation:
- R3. Hydration mismatch(
useVotingPeriodStatus): クライアント時計依存で SSR と差が出る可能性- Mitigation:
design.mdで voting spec に課す制約として明文化済み(初期isOpen=false/useEffectで再判定)
- Mitigation:
- R4. テスト基盤の未整備: Vitest / Playwright が
package.jsonに無く、tasks.md4.x がそのままでは実行不能- Mitigation: design phase 中に「テスト基盤導入」が
data-modelまたは専用 spec の責務か、home/tasks.md内サブタスクかを決める
- Mitigation: design phase 中に「テスト基盤導入」が
- R5. Storybook 未導入:
steering/structure.mdが必須としているがリポジトリに存在しない- Mitigation: 同上。導入は別 spec(または共通 chore)として切り出す候補あり
5. Recommendations for Design Phase
Preferred Approach
- Option B(spec 通りにゼロから構築)を採用 することを推奨。
- 理由: 現行コードは空に近く、再利用すべき既存実装が無い。
design.mdとtasks.mdがすでに本案を前提に書かれており、改変コストが最小。
Order of Implementation(上流依存を踏まえた推奨順)
data-model完了(lib/voting-period.ts/lib/microcms.ts/lib/prisma.ts/features/cms/services/**/features/candidates/services/voteAggregationService.ts/next.config.tsのremotePatterns更新)candidate-rankingのfeatures/candidates/utils/rankCandidates.ts確定votingのuseVoteFlow/useVotingPeriodStatus/<PurchaseFlowContainer>確定- 本 spec の
tasks.mdを Foundation → Core → Integration → 検証 の順で実行
Key Decisions to Confirm in Design Phase
- 投票期間表示の整形パターン(
Intl.DateTimeFormat引数の確定文言) app/error.tsxでInvalidVotingPeriodErrorをerror.causeから識別する方式(他のエラーと文言を出し分けるかどうか)- Confetti の DOM 配置(
HomePageClient直下に置き z-index で最前面化、ScrollToTopButtonとの競合解消) - テスト基盤(Vitest / Playwright / Storybook)の導入 spec 配属
Research Items to Carry Forward
- Server Component の単体テスト戦略(Vitest だけでは Server Component を直接 render できないため、services をモックして関数を切り出す形が現実的)
useOptimisticの React 19 安定版 API シグネチャ(reducer の return 型 vs 直接更新)- microCMS の
customRequestInitをnext.revalidateと組み合わせる際の TTL 数値(data-model委譲だが home 描画の鮮度感に影響)
Carryover for tasks.md(必要なら spec 再生成時に追記)
next.config.tsのimages.remotePatterns確認チェック(home 単独では編集禁止)- テスト基盤(Vitest / Playwright)導入の前提タスク(現行 tasks 4.x が前提を欠く)