Skip to content

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

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

このプロジェクトは Headless UI パターン + Feature-based 構造 を採用し、デザイン(見た目)と機能(ロジック)を明確に分離する。

3層モデル

┌─────────────────────────────────────────────────────────┐
│ app/ (Pages)         画面の組み立て・データ取得・レイアウト  │
├─────────────────────────────────────────────────────────┤
│ features/ (Logic)    機能ロジック(フック・サービス・型)    │
├─────────────────────────────────────────────────────────┤
│ components/ (UI)     表示専用コンポーネント(presentation)│
└─────────────────────────────────────────────────────────┘
責務
app/ページ単位の組み立て、データ取得、メタデータ生成page.tsx 内で useVoteAction<VoteButton> を組み合わせる
features/ビジネスロジック・状態管理・API通信useVoteAction.ts, voteService.ts, VotingContext.tsx
components/propsを受け取って表示するだけ<VoteButton canVote label 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: 見た目)

├── app/                           # Pages層: 画面組み立て
│   ├── layout.tsx
│   ├── globals.css                # CSS変数 (design tokens)
│   ├── providers.tsx              # Provider集約 ("use client")
│   ├── page.tsx                   # Home
│   ├── candidate/[id]/page.tsx    # 候補者詳細
│   ├── ranking/page.tsx           # ランキング
│   └── api/                       # 将来のRoute Handlers

├── features/                      # Logic層: ビジネスロジック
│   ├── auth/
│   │   ├── contexts/AuthContext.tsx
│   │   ├── hooks/
│   │   │   ├── useAuth.ts
│   │   │   └── useLoginFlow.ts
│   │   ├── services/authService.ts
│   │   └── types.ts
│   ├── voting/
│   │   ├── contexts/VotingContext.tsx
│   │   ├── hooks/
│   │   │   ├── useVoteAction.ts          # 投票実行
│   │   │   ├── useFreeVoteStatus.ts      # 無料投票可否
│   │   │   └── useVoteCountdown.ts       # 残り時間
│   │   ├── services/voteService.ts
│   │   └── types.ts
│   ├── candidates/
│   │   ├── hooks/
│   │   │   ├── useCandidates.ts
│   │   │   ├── useCandidateListWithRank.ts  # ID昇順 + 順位付与(home画面用)
│   │   │   └── useCandidateRanking.ts        # 得票降順 + 順位付与(ranking画面用)
│   │   ├── services/candidateService.ts
│   │   ├── data/candidates.ts            # デモ用静的データ
│   │   └── types.ts
│   ├── voters/
│   │   ├── hooks/useVoterRanking.ts
│   │   ├── data/voters.ts
│   │   └── types.ts
│   └── payment/
│       ├── hooks/usePurchaseFlow.ts
│       ├── services/stripeService.ts
│       ├── data/creditPackages.ts
│       └── types.ts

├── components/                    # UI層: 表示専用
│   ├── ui/                        # shadcn/ui プリミティブ
│   ├── layout/
│   │   ├── Footer.tsx
│   │   ├── PageBackground.tsx     # 装飾背景
│   │   ├── ScrollToTop.tsx
│   │   └── ScrollToTopButton.tsx
│   ├── candidate/
│   │   ├── CandidateCard.tsx
│   │   ├── CandidateImageSlider.tsx
│   │   ├── CandidateInfo.tsx
│   │   ├── CandidateBadge.tsx
│   │   └── CandidateTag.tsx
│   ├── ranking/
│   │   ├── RankingList.tsx
│   │   ├── RankingItem.tsx
│   │   └── RankIcon.tsx
│   ├── voter/
│   │   ├── VoterRanking.tsx
│   │   └── VoterRankItem.tsx
│   ├── voting/
│   │   ├── VoteButton.tsx
│   │   ├── FreeVoteButton.tsx
│   │   ├── PaidVoteButton.tsx
│   │   └── VoteCountdown.tsx
│   ├── modals/
│   │   ├── SocialLoginModal.tsx
│   │   ├── PurchaseCreditsModal.tsx
│   │   ├── CreditPackageCard.tsx
│   │   └── StripePaymentModal.tsx
│   └── effects/
│       └── Confetti.tsx

├── containers/                    # (任意) UI と feature を繋ぐ薄い層
│   ├── CandidateCardContainer.tsx
│   └── VoteButtonContainer.tsx

├── design-tokens/                 # デザイントークン
│   ├── colors.ts                  # TS でも参照可能にする
│   ├── typography.ts
│   └── tokens.css                 # CSS 変数定義(globals.css から import)

├── lib/                           # 汎用ユーティリティ
│   ├── cn.ts
│   └── date.ts

├── public/
│   └── images/

├── .storybook/                    # Storybook 設定
└── stories/                       # コンポーネントカタログ(components/ と対応)

レイヤーごとの実装ルール

features/ レイヤー (機能)

  • 公開するもの: カスタムフック、サービス関数、型
  • 公開しないもの: JSX、HTML、CSSクラス、UIプリミティブ
  • 依存先: 他 features の hooks(限定的)、lib、外部API
  • テスト: フックは @testing-library/reactrenderHook、サービスは Vitest 単体テスト
ts
// Good: features/voting/hooks/useVoteAction.ts
export function useVoteAction(candidateId: number) {
  const { isAuthenticated } = useAuth();
  const { canVoteFree, executeFreeVote } = useVoting();
  
  return {
    canVote: canVoteFree(),
    requiresLogin: !isAuthenticated,
    vote: () => executeFreeVote(candidateId),
  };
}

// Bad: フック内で JSX を返す
export function useVoteButton() {
  return <button>投票</button>; // NG
}

components/ レイヤー (UI)

  • 公開するもの: React コンポーネント
  • 必須: すべての props を型定義、Storybookストーリー、デフォルト値
  • 禁止: useAuth(), useVoting() などの Context 消費、fetch()、ビジネスロジック
  • 許可: useState (UI内部状態のみ)、useRef、アニメーション制御
tsx
// Good: 表示専用、propsのみで全制御可能
type VoteButtonProps = {
  variant: 'free' | 'paid';
  canVote: boolean;
  label: string;
  onClick: () => void;
};
export function VoteButton({ variant, canVote, label, onClick }: VoteButtonProps) {
  return (
    <button
      disabled={!canVote}
      onClick={onClick}
      className={cn(
        'px-6 py-3 rounded-full',
        variant === 'free' ? 'bg-[var(--gold)] text-white' : 'border-2 border-[var(--gold)]'
      )}
    >
      {label}
    </button>
  );
}

// Bad: components から features を呼ぶ
export function VoteButton() {
  const { canVoteFree } = useVoting(); // NG
  return <button disabled={!canVoteFree()}>投票</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} />;
}

// app/_components/CandidateGrid.tsx (Client Component)
'use client';
import { useVoteAction } from '@/features/voting/hooks/useVoteAction';
import { CandidateCard } from '@/components/candidate/CandidateCard';

export function CandidateGrid({ candidates }: Props) {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
      {candidates.map((c) => (
        <CandidateCardContainer key={c.id} candidate={c} />
      ))}
    </div>
  );
}

containers/ レイヤー (任意・接着剤)

ロジックフックと UI が複雑に絡む場合のみ使う。シンプルなページなら不要。

tsx
// containers/CandidateCardContainer.tsx
'use client';
import { useVoteAction } from '@/features/voting/hooks/useVoteAction';
import { CandidateCard } from '@/components/candidate/CandidateCard';

export function CandidateCardContainer({ candidate }: Props) {
  const vote = useVoteAction(candidate.id);
  return <CandidateCard candidate={candidate} {...vote} />;
}

デザイントークン管理

単一ソースの原則

すべての色・タイポ・スペーシングは 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/VoteButton.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { VoteButton } from '@/components/voting/VoteButton';

const meta: Meta<typeof VoteButton> = {
  title: 'Voting/VoteButton',
  component: VoteButton,
};
export default meta;

export const FreeVoteAvailable: StoryObj<typeof VoteButton> = {
  args: { variant: 'free', canVote: true, label: '無料で投票する(一日一回)' },
};
export const FreeVoteDisabled: StoryObj<typeof VoteButton> = {
  args: { variant: 'free', canVote: false, label: '投票済み(あと5時間20分で再投票可能)' },
};
export const PaidVote: StoryObj<typeof VoteButton> = {
  args: { variant: 'paid', canVote: true, 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つのドキュメントすべてと、各レイヤーのコードを更新

ファイル命名規則

  • コンポーネント: PascalCaseCandidateCard.tsx
  • フック: camelCase + use prefixuseVoteAction.ts
  • サービス: camelCase + ServicevoteService.ts
  • 型: PascalCaseCandidate, Vote, User
  • ストーリー: <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 / admin / ui)

feature単位(candidate/, voting/, modals/ など)ではなく、利用面で分割する。

components/
├── ui/        # shadcn/ui プリミティブ (button, card, dialog, ...)
├── public/    # 投票サイト側の表示専用コンポーネント
│              # CandidateCard, CandidateGrid, FreeVoteButton, PaidVoteButton,
│              # PurchaseCreditsModal, SocialLoginModal, Confetti, HomeHero,
│              # PageBackground, Footer, ScrollToTopButton
└── admin/     # 管理画面側のコンポーネント
              # AdminNavbar, CandidateForm, ImageUploader,
              # CandidateDeleteButton, LoginForm

理由: 投票サイトと管理画面は UI トーン・ターゲット利用者・依存ライブラリが大きく異なるため、最初の分岐を「public / admin」にすることで再利用範囲が明確になる。

ページローカル _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/ または components/admin/
  • 単一ページ専用の組み立て・分割 → app/<route>/_components/

管理画面 (admin) の構造

管理画面は app/admin/ 配下に集約し、Server Actions を _actions/ に分離する。

app/admin/
├── layout.tsx              # 管理画面レイアウト + 認証ガード
├── login/page.tsx
├── candidates/
│   ├── page.tsx            # 一覧
│   └── new/page.tsx        # 新規作成
└── _actions/
    ├── candidates.ts       # 候補者CRUD Server Actions
    └── images.ts           # 画像 Server Actions

_actions/ 配下のファイルは先頭に "use server" を記述し、app/admin/ 内のフォームから直接呼び出す。

features/ の現状

当初設計の voters/, payment/ は未実装。実装済みは auth/, voting/, candidates/ の3つで、薄い構成(contexts + 必要最小限の hooks/types)に留まっている。サービス層は現状 Server Actions / Route Handler 側にあり、features/*/services/ は将来の拡張ポイント。

features/
├── auth/contexts/AuthContext.tsx     # クライアント状態(モック含む)
├── voting/
│   ├── contexts/VotingContext.tsx    # 無料投票クールダウン管理(JST 0:00 リセット)
│   └── hooks/
│       ├── useVoteFlow.ts            # ログイン/購入モーダル + Confetti のフロー制御
│       └── useFreeVoteStatus.ts      # 残り時間カウントダウン
└── candidates/
    └── hooks/useCandidateRanking.ts  # 得票降順 + 順位付与

lib/ の現状

当初設計の lib/contexts/, lib/data/, lib/types/ は採用していない。

  • Context → features/<domain>/contexts/
  • 型 → features/<domain>/types.ts
  • データ → Prisma + DB
  • lib/真に汎用的なユーティリティ のみ(現状は prisma.ts(Prisma Client 共有インスタンス) と utils.ts(cn() 等))

ルートレベルのファイル

  • auth.ts — NextAuth.js v5 の設定とハンドラを集約してエクスポート
  • proxy.ts — 開発用プロキシ設定
  • prisma.config.ts — Prisma 7 の設定(seed 等)

未実装の予定パターン

以下は当初設計に含まれているが現時点では未実装。新規実装時に検討する。

  • containers/ レイヤー(features と components の接着剤)
  • .storybook/ および stories/(コンポーネントカタログ)
  • Vitest / Playwright のテストファイル

import エイリアス(実態)

tsconfig.jsonpaths@/*, @/features/*, @/components/*, @/design-tokens/* を定義済み。新規 import は基本的にこれらのエイリアスを使う。