Skip to content

プロジェクト構造とコーディング規約

設計の基本方針: デザインと機能の分離

このプロジェクトは 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.mdWHAT(何を実現するか) のみを記述し、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」
ブラウザ APIlocalStorage, 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/reactrenderHook、サービスは 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 設計、状態バリエーションの洗い出し

レビュー手順

  1. Figma レビュー: ui-design.md と Figma の整合性を確認
  2. Storybook レビュー: 実装された components/ をデザイナーが Storybook で確認
  3. 統合レビュー: 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.jsonpaths@/*, @/features/*, @/components/*, @/design-tokens/* を定義済み。新規 import は基本的にこれらのエイリアスを使う。