テーマ
プロジェクト構造とコーディング規約
設計の基本方針: デザインと機能の分離
このプロジェクトは Headless UI パターン + Feature-based 構造 を採用し、デザイン(見た目)と機能(ロジック)を明確に分離する。
3層モデル
┌─────────────────────────────────────────────────────────┐
│ app/ (Pages) 画面の組み立て・データ取得・レイアウト │
├─────────────────────────────────────────────────────────┤
│ features/ (Logic) 機能ロジック(フック・サービス・型) │
├─────────────────────────────────────────────────────────┤
│ components/ (UI) 表示専用コンポーネント(presentation) │
└─────────────────────────────────────────────────────────┘| 層 | 責務 | 例 |
|---|---|---|
| app/ | ページ単位の組み立て、データ取得、メタデータ生成 | page.tsx 内で usePurchaseFlow と <PaidVoteButton> を組み合わせる |
| features/ | ビジネスロジック・状態管理・API通信 | usePurchaseFlow.ts, purchaseService.ts, useNickname.ts |
| components/ | propsを受け取って表示するだけ | <PaidVoteButton onClick> |
分離の原則
- components/ 配下のコンポーネントは features/ をインポートしてはならない(依存方向は app → features / app → components のみ)
- components/ は React Hooks の useState / useReducer などローカル状態のみ許可。Context消費・API呼び出し・ビジネスロジックは禁止
- features/ のフックはUIを返さない。
{ data, action }形式の値・関数のみを返す - app/ が両者を組み合わせる場になる
ディレクトリ構成
miss-world-japan-voting/
├── .kiro/
│ ├── steering/ # プロジェクト全体の方針
│ └── specs/ # 機能・画面ごとの仕様
│ └── <feature>/
│ ├── requirements.md # 機能要件(What — 実装非依存)
│ ├── design.md # 技術設計(How: ロジック・データフロー)
│ └── ui-design.md # ビジュアルデザイン仕様(How: 見た目)
│
│ # 各 spec の 3 ファイル分担は本ドキュメントの定義に従う。
│ # 各 requirements.md では "ファイル配置・型定義・実装詳細は design.md、
│ # ビジュアル仕様は ui-design.md を参照" といった汎用注記を再掲しない。
│ # 別 spec の ui-design.md を流用するなど spec 固有のクロス参照のみ各 spec に記述する。
│ # requirements.md の実装非依存ルールは下記「requirements.md の書き方」を参照。
│
├── app/ # Pages層: 画面組み立て
│ ├── layout.tsx
│ ├── globals.css # CSS変数 (design tokens)
│ ├── providers.tsx # Provider集約 ("use client")
│ ├── page.tsx # Home
│ ├── candidate/[id]/page.tsx # 候補者詳細
│ ├── ranking/page.tsx # ランキング
│ ├── actions/ # Server Actions
│ │ └── purchase.ts # PaymentIntent 作成、決済状態取得
│ └── api/ # Route Handlers
│ └── webhooks/stripe/route.ts
│
├── features/ # Logic層: ビジネスロジック
│ ├── voting/
│ │ ├── hooks/
│ │ │ ├── useVoteFlow.ts # 投票フロー(購入モーダル + Confetti)
│ │ │ └── useNickname.ts # ニックネーム入力 + localStorage 永続化
│ │ └── types.ts
│ ├── candidates/
│ │ ├── hooks/
│ │ │ ├── useCandidateListWithRank.ts # ID昇順 + 順位付与(home画面用)
│ │ │ └── useCandidateRanking.ts # 得票降順 + 順位付与(ranking画面用)
│ │ └── types.ts
│ ├── cms/ # microCMS クライアントラッパー
│ │ ├── services/
│ │ │ ├── candidateService.ts # getCandidates / getCandidate
│ │ │ └── creditPackageService.ts # getActiveCreditPackages
│ │ └── types.ts
│ ├── voters/
│ │ ├── services/voterService.ts # 応援者(ニックネーム単位)ランキング集計
│ │ └── types.ts
│ └── payment/
│ ├── hooks/
│ │ ├── usePurchaseCart.ts # 票数パッケージの組み合わせ(数量管理)
│ │ └── usePurchaseFlow.ts # 状態機械 (select → payment → success/error)
│ ├── services/
│ │ └── purchaseService.ts
│ └── types.ts
│
├── components/ # UI層: 表示専用
│ ├── ui/ # shadcn/ui プリミティブ
│ └── public/ # 投票サイト側の表示専用コンポーネント
│ ├── CandidateCard.tsx
│ ├── CandidateGrid.tsx
│ ├── PaidVoteButton.tsx
│ ├── PurchaseCreditsModal.tsx # 複数パッケージの組み合わせ選択 UI
│ ├── CreditPackageCard.tsx
│ ├── NicknameInput.tsx # ニックネーム入力(localStorage 連携)
│ ├── StripePaymentModal.tsx
│ ├── Confetti.tsx
│ ├── HomeHero.tsx
│ ├── PageBackground.tsx
│ ├── Footer.tsx
│ └── ScrollToTopButton.tsx
│
├── design-tokens/ # デザイントークン
│ ├── colors.ts
│ ├── typography.ts
│ └── tokens.css
│
├── lib/ # 汎用ユーティリティ
│ ├── prisma.ts # Prisma Client 共有インスタンス (Neon)
│ ├── microcms.ts # microCMS Client 共有インスタンス
│ ├── voting-period.ts # 環境変数からの投票期間取得
│ ├── date.ts
│ └── utils.ts # cn()
│
├── public/
│ └── images/
│
├── .storybook/ # Storybook 設定
└── stories/ # コンポーネントカタログapp/admin/ features/admin/ components/admin/ app/api/auth/[...nextauth]/ app/api/images/[id]/ auth.ts は 保有しない。運営者向け機能は microCMS の管理画面に完全委譲し、画像配信は microCMS CDN に委譲したため。
requirements.md の書き方(実装非依存ルール)
.kiro/specs/<feature>/requirements.md は WHAT(何を実現するか) のみを記述し、HOW(どう実現するか) を含めない。実装詳細は design.md に置き、要件と設計を分離することで、ベンダー差し替え時にも requirements.md を書き換えずに済む構造を維持する。
requirements.md に書かないもの
| カテゴリ | 例 | requirements.md での書き方 |
|---|---|---|
| ベンダー名・SaaS 名 | microCMS, Neon, Vercel, Stripe | 「コンテンツ管理基盤」「取引データストア」「デプロイ環境設定」「決済プロバイダ」 |
| フレームワーク機能名 | Server Component, Server Action, Route Handler, next/image, Next.js Data Cache | 「サーバー側」「画像配信網」「キャッシュ」 |
| 環境変数名 | MICROCMS_API_KEY_READONLY, VOTING_PERIOD_START | 「読み取り専用 API キー」「開始日時の設定値」 |
| テーブル名・カラム名・コレクション名 | votes テーブル, candidate_id, creditPackages コレクション | 「投票エンティティ」「対象候補者」「票数パッケージコレクション」 |
| プロダクト固有 API・機能 | Stripe PaymentIntent, Stripe Webhook, Stripe Link, Payment Element | 「決済プロバイダの取引識別子」「決済完了通知」「ワンクリック決済」「埋め込み決済 UI」 |
| ブラウザ API | localStorage, DOM | 「ブラウザ側に永続化する」「当アプリ側に保持しない」 |
| コンポーネント名・型名・関数名 | Purchase Credits Modal, usePurchaseFlow | 「購入モーダル」「購入フロー」 |
| 準拠カテゴリ等の細目 | SAQ-A, ARIA | 「カード情報を当アプリ側で保持しない」「支援技術にも状態を伝達する」 |
| ライブラリ名・パッケージ名 | Prisma, NextAuth, shadcn/ui | (記述しない) |
requirements.md に書いてよいもの
- ドメイン用語(候補者、投票、票数パッケージ、応援者ランキング 等)
- 業務ルール(同得票時は ID 昇順で順位を確定する 等)
- ユーザーインタラクションの抽象表現(購入モーダル、決済モーダル、ニックネーム入力 等)
- 業務上の固有名(「Miss World Japan 2026」というサイト名 等)
- ベンダー中立な国際標準名(ISO 8601、JPEG / PNG / WebP 等)
例外
- spec の 概要(Overview) で背景や採用したアーキテクチャの全体像を伝える目的でベンダー名を補助的に挙げることは可。ただし各 Feature の Purpose / Scope / Acceptance Criteria はベンダー中立な表現に揃える。
- 受け入れ基準を 「外部システムへの委譲」 として表現する場合、システム種別(コンテンツ管理基盤、決済プロバイダ等)までで止め、具体的なプロダクト名は記述しない。
違反検出と修正
- レビュー時は上記表の左列の語を検索し、ヒットしたら
design.mdへの移管または抽象化を行う。 - 具体的な API 呼び出し・スキーマ・データフローは各 spec の
design.mdに、ベンダー選定の根拠はsteering/tech.mdに書く。
レイヤーごとの実装ルール
features/ レイヤー (機能)
- 公開するもの: カスタムフック、サービス関数、型
- 公開しないもの: JSX、HTML、CSSクラス、UIプリミティブ
- 依存先: 他 features の hooks(限定的)、lib、外部API
- テスト: フックは
@testing-library/reactのrenderHook、サービスは Vitest 単体テスト
ts
// Good: features/voting/hooks/usePurchaseCart.ts
export function usePurchaseCart() {
const [items, setItems] = useState<CartItem[]>([]);
const totalCredits = items.reduce((sum, i) => sum + i.credits * i.quantity, 0);
const totalAmountJpy = items.reduce((sum, i) => sum + i.priceJpy * i.quantity, 0);
// ...
return { items, totalCredits, totalAmountJpy, addItem, removeItem };
}
// Bad: フック内で JSX を返す
export function usePurchaseCartUI() {
return <div>カート</div>; // NG
}components/ レイヤー (UI)
- 公開するもの: React コンポーネント
- 必須: すべての props を型定義、Storybookストーリー、デフォルト値
- 禁止: features のフック消費、
fetch()、ビジネスロジック - 許可:
useState(UI内部状態のみ)、useRef、アニメーション制御
tsx
// Good: 表示専用、propsのみで全制御可能
type PaidVoteButtonProps = {
label: string;
onClick: () => void;
};
export function PaidVoteButton({ label, onClick }: PaidVoteButtonProps) {
return (
<button
onClick={onClick}
className="px-6 py-3 rounded-full bg-[var(--gold)] text-white"
>
{label}
</button>
);
}
// Bad: components から features を呼ぶ
export function PaidVoteButton() {
const { startPurchase } = usePurchaseFlow(); // NG
return <button onClick={startPurchase}>投票</button>;
}app/ レイヤー (ページ・組み立て)
- 責務: features のフックと components を組み合わせ、画面を構築
- データ取得: Server Component で初期データを取得し、Client Component に props で渡す
- 副作用: Server Actions / Route Handlers から呼ぶ
tsx
// app/page.tsx (Server Component)
import { getCandidates } from '@/features/candidates/services/candidateService';
import { CandidateGrid } from './_components/CandidateGrid';
export default async function HomePage() {
const candidates = await getCandidates();
return <CandidateGrid candidates={candidates} />;
}デザイントークン管理
単一ソースの原則
すべての色・タイポ・スペーシングは design-tokens/ で定義し、CSS と TS の両方からアクセスできるようにする。
css
/* design-tokens/tokens.css → app/globals.css で @import */
:root {
--gold: #D4AF37;
--gold-light: #E8D7A2;
--gold-dark: #B8941F;
--rose-gold: #E8B4A8;
--ivory: #FFFFF0;
--charcoal: #2C2C2C;
--font-display: 'Cormorant Garamond', serif;
--font-body: 'Jost', sans-serif;
}ts
// design-tokens/colors.ts (TS 参照用)
export const colors = {
gold: 'var(--gold)',
goldLight: 'var(--gold-light)',
goldDark: 'var(--gold-dark)',
roseGold: 'var(--rose-gold)',
ivory: 'var(--ivory)',
charcoal: 'var(--charcoal)',
} as const;
export type ColorToken = keyof typeof colors;Figma との同期
- Figma 側のデザイントークンを single source of truth とする
- 変更が発生したら
design-tokens/tokens.cssを手動 or CLI(Style Dictionary など)で同期 - 将来的には Figma Tokens Plugin → JSON → コード変換のパイプラインを構築
命名規則
- 色: 役割名(
--primary,--success)と装飾名(--gold,--rose-gold)を併存させる - フォント:
--font-display,--font-body - 角丸:
--radius-sm,--radius-md,--radius-lg,--radius-full - シャドウ:
--shadow-card,--shadow-modal
Storybook によるコンポーネントカタログ
すべての components/ 配下のコンポーネントは Storybook ストーリーを必須とする。
tsx
// stories/voting/PaidVoteButton.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { PaidVoteButton } from '@/components/public/PaidVoteButton';
const meta: Meta<typeof PaidVoteButton> = {
title: 'Voting/PaidVoteButton',
component: PaidVoteButton,
};
export default meta;
export const Default: StoryObj<typeof PaidVoteButton> = {
args: { label: '投票する' },
};これにより:
- デザイナーが Storybook 上で全状態を確認できる
- ロジックを通さずに UI 単体のレビューが可能
- ビジュアルリグレッションテスト(Chromatic 等)に乗せやすい
デザイナーとのハンドオフ
役割分担
| 担当 | アウトプット |
|---|---|
| デザイナー | Figma デザイン、ui-design.md(仕様)、デザイントークン |
| エンジニア | features/ ロジック、components/ 実装、Storybook ストーリー |
| 協働 | コンポーネント命名、props 設計、状態バリエーションの洗い出し |
レビュー手順
- Figma レビュー: ui-design.md と Figma の整合性を確認
- Storybook レビュー: 実装された components/ をデザイナーが Storybook で確認
- 統合レビュー: app/ 上でロジック含む完成形を最終確認
仕様変更時のフロー
- 見た目だけの変更 → ui-design.md と components/ のみ更新(features は触らない)
- 機能だけの変更 → requirements.md / design.md と features/ のみ更新(components は触らない)
- 両方の変更 → 3つのドキュメントすべてと、各レイヤーのコードを更新
ファイル命名規則
- コンポーネント: PascalCase(
CandidateCard.tsx) - フック: camelCase + use prefix(
usePurchaseFlow.ts) - サービス: camelCase + Service(
purchaseService.ts) - 型: PascalCase(
Candidate,Vote,Purchase) - ストーリー:
<ComponentName>.stories.tsx - テスト:
<ComponentName>.test.tsx
インポートエイリアス
json
{
"compilerOptions": {
"paths": {
"@/*": ["./*"],
"@/features/*": ["./features/*"],
"@/components/*": ["./components/*"],
"@/design-tokens/*": ["./design-tokens/*"]
}
}
}Git運用
- ブランチ命名:
feature/<spec-name>、design/<area>、refactor/<scope> - コミット: Conventional Commits(
feat:,fix:,style:,refactor:,docs:) - PR レビュー観点: 「features と components の依存方向が正しいか」を必ずチェック
現行実装メモ (Implementation Notes)
上記「ディレクトリ構成」は当初設計時の理想形を示しているが、現行コードベースでは以下の実装パターンが採用されている。新規コードを書く際はこちらの実際のパターンに従うこと。
components/ の分割: 用途別 (public / ui)
feature単位ではなく、利用面で分割する。本サイトは公開画面のみを保有するため components/admin/ は持たない(運営者向け UI は microCMS が提供)。
components/
├── ui/ # shadcn/ui プリミティブ (button, card, dialog, ...)
└── public/ # 投票サイト側の表示専用コンポーネント
# CandidateCard, CandidateGrid, PaidVoteButton,
# PurchaseCreditsModal, CreditPackageCard, NicknameInput,
# StripePaymentModal, Confetti, HomeHero,
# PageBackground, Footer, ScrollToTopButton理由: 投票サイトの公開画面のみを保有することにより、public/ と ui/ の 2 区分で十分。運営者向けコンポーネントを保有しない方針を再導入時の境界として明示する。
ページローカル _components/ パターン
そのページでしか使わないクライアントコンポーネントは、ページと同階層の _components/ に置く(Next.js App Router の慣習でアンダースコア始まりはルート対象外)。
app/
├── _components/HomePageClient.tsx
├── candidate/[id]/_components/
│ ├── CandidateDetailClient.tsx
│ ├── CandidateImageSlider.tsx
│ ├── InfoBadgeRow.tsx
│ ├── PRMessage.tsx
│ └── TagList.tsx
└── ranking/_components/
├── RankingItem.tsx
└── RankingPageClient.tsx判断基準:
- 複数画面で再利用される表示部品 →
components/public/ - 単一ページ専用の組み立て・分割 →
app/<route>/_components/
運営者向け管理機能の非保有
本サイトは app/admin/ 配下、運営者向け Server Actions、運営者認証(auth.ts)を 保有しない。運営者向けのコンテンツ管理 UI・認証・権限・監査ログは microCMS が提供するため、本サイト側に該当ディレクトリ・ファイルを再導入しない方針とする。
features/ の現状
当初設計の auth/ および admin/ は廃止された(一般ユーザーのログイン機能を提供せず、運営者管理は microCMS に委譲したため)。voting/, candidates/, cms/, payment/, voters/ を中心に薄い構成で運用する。
features/
├── voting/
│ ├── hooks/
│ │ ├── useVoteFlow.ts # 購入モーダル + Confetti のフロー制御
│ │ └── useNickname.ts # ニックネーム入力 + localStorage 永続化
│ └── types.ts
├── candidates/
│ └── hooks/
│ └── useCandidateRanking.ts # 得票降順 + 順位付与
├── cms/ # microCMS クライアントラッパー (services 中心)
│ ├── services/
│ │ ├── candidateService.ts # getCandidates / getCandidate
│ │ └── creditPackageService.ts # getActiveCreditPackages
│ └── types.ts
├── payment/
│ ├── hooks/
│ │ ├── usePurchaseCart.ts # 複数パッケージの数量管理
│ │ └── usePurchaseFlow.ts # PaymentIntent 作成 → Webhook 待ち合わせ
│ └── services/
│ └── purchaseService.ts
└── voters/
└── services/voterService.ts # ニックネーム集計lib/ の現状
当初設計の lib/contexts/, lib/data/, lib/types/ は採用していない。
- 型 →
features/<domain>/types.ts - データ → Prisma (Neon) + microCMS SDK + 環境変数
lib/は 真に汎用的なユーティリティ のみ。代表的な構成:prisma.ts(Neon 用 Prisma Client 共有インスタンス)microcms.ts(microCMS Client 共有インスタンス)voting-period.ts(環境変数からの投票期間取得)date.ts(JST 境界判定)utils.ts(cn()等)
ルートレベルのファイル
proxy.ts— 開発用プロキシ設定prisma.config.ts— Prisma 7 の設定(seed 等、Neon 対象)auth.tsは 保有しない(運営者認証は microCMS、一般ユーザーは認証なし)
未実装の予定パターン
以下は当初設計に含まれているが現時点では未実装。新規実装時に検討する。
.storybook/およびstories/(コンポーネントカタログ)- Vitest / Playwright のテストファイル
import エイリアス(実態)
tsconfig.json の paths に @/*, @/features/*, @/components/*, @/design-tokens/* を定義済み。新規 import は基本的にこれらのエイリアスを使う。