# HIREBASE — 要件定義書 兼 Claude Code 実装指示書

**版数** 2.0（DBレス構成）／ **作成日** 2026-08-13 ／ **発行** ソウゾウ合同会社
**用途** 「爆速アプリ開発」LP 実演デモ

---

## このドキュメントの使い方（Claude Code へ）

これ1枚が、規約・仕様・タスクのすべて。リポジトリ直下に `CLAUDE.md` として置き、毎回読み込むこと。

- **実装は「11. 実装タスク」の順に進める。フェーズを飛ばさない。**
- 各タスクの括弧内は要件ID。着手前に該当章を読むこと。
- **仕様がここに書かれていない場合は、推測で実装せず質問する。**
- スコープ外（3章 Won't have）は、思いついても実装しない。
- 「デモだから」を理由に品質を落とす判断はしない。

---

## 目次

1. [プロジェクト概要](#1-プロジェクト概要)
2. [アーキテクチャ方針 — データベースを持たない](#2-アーキテクチャ方針--データベースを持たない)
3. [スコープ定義](#3-スコープ定義)
4. [ロールとデモ切替](#4-ロールとデモ切替)
5. [画面一覧とユーザーフロー](#5-画面一覧とユーザーフロー)
6. [機能要件](#6-機能要件)
7. [データ設計](#7-データ設計)
8. [デザイン要件](#8-デザイン要件)
9. [技術要件・ディレクトリ構成](#9-技術要件ディレクトリ構成)
10. [非機能要件](#10-非機能要件)
11. [実装タスク](#11-実装タスク)
12. [受入基準](#12-受入基準)

---

# 1. プロジェクト概要

## 背景と目的

「爆速アプリ開発」LPの訪問者は、価格（150万円）に対して「その金額で何がどこまで作れるのか」を判断できないままCTAに到達する。文章と静止画では、実装粒度・デザイン品質・機能密度が伝わらない。

そこで、LPから直接触れる**実際に動作するプロダクト**を用意し、「この水準のものが、この期間・この金額で出てくる」を体験として提示する。

| 目的 | 内容 |
|---|---|
| 実装力の証明 | 検索・応募・スカウト・メッセージ・分析まで、実サービス相当の機能密度を持つことを示す |
| デザイン品質の証明 | テンプレート然としない、設計されたUIであることを1画面目で伝える |
| 横展開の資産化 | ドメイン差し替え（介護／特定技能／建設等）で、そのまま案件のベースにできる構造にする |

## プロダクト定義

| 項目 | 内容 |
|---|---|
| プロダクト名 | HIREBASE（ハイヤーベース）※仮称 |
| 一言定義 | 企業からのスカウトが届く、求人プラットフォーム |
| 提供形態 | レスポンシブWebアプリ。**フロントエンド完結（サーバー側の永続化なし）** |
| 想定利用者 | LP訪問者。誰でも即座に、登録なしで全ロールを体験できる |
| 対応言語 | 日本語 |

## プロダクトコンセプト

> **「検索する求人サイト」から「届く求人サイト」へ。**
>
> 求職者は職務経歴を1度書けば、あとは企業からのスカウトを待つだけ。企業は条件に合う候補者を検索し、直接アプローチできる。応募・スカウトの両方向の導線を1つのメッセージ基盤に統合する。

## デモとしての成功条件

1. **触った瞬間に「作り込まれている」と分かる** — 空の画面が1つもない。全画面にリアルなデータが入っている
2. **ロールを跨いだ体験が繋がる** — 求職者として応募 → 企業に切り替えると、その応募が届いている
3. **迷わない** — 何ができるか、どこを見ればいいかが常に分かる
4. **リセットできる** — 誰が触った後でも、初期状態に戻せる

---

# 2. アーキテクチャ方針 — データベースを持たない

## 基本方針

**バックエンドとデータベースを持たない。すべてブラウザ内で完結させる。**

| 一般的な構成 | 本プロジェクト |
|---|---|
| PostgreSQL / Supabase | シードデータ（TypeScript）+ ブラウザ内ストア |
| 認証（OTP・OAuth） | デモ用ロール切替（ワンクリック） |
| Server Actions / API | ストア上の同期的な操作 |
| メール送信（Resend） | アプリ内通知センターに配信されたことを表示 |
| 決済（Stripe） | プラン選択UIのみ（購入完了までを画面上で再現） |
| ファイルアップロード | ブラウザ内で Object URL 化して表示 |

## なぜこの構成にするか

- **LPデモとして最適** — 訪問者は登録もログインもせずに、全機能を即座に触れる
- **個人情報を一切預からない** — 入力されたデータはブラウザから出ない。法令リスクとプライバシーポリシーの負担がゼロになる
- **開発速度** — 認証・RLS・マイグレーション・メール基盤の実装が不要になり、**UIと体験の作り込みに全時間を投下できる**
- **運用コストゼロ** — Vercel の静的配信のみ。DBの月額も、バックアップも、障害対応も発生しない

## 差し替え可能性の担保（重要）

**データアクセスは必ず `lib/repo/` のリポジトリ層を経由すること。** コンポーネントからストアを直接触らない。

```
[ コンポーネント ]
        ↓  呼ぶのはこの層だけ
[ lib/repo/*.ts ]   ← 全メソッドを async にしておく
        ↓
[ lib/store/*.ts ]  ← Zustand + persist（localStorage）
        ↓
[ lib/seed/*.ts ]   ← 初期データ
```

リポジトリの各メソッドは、中身が同期処理でも **`async` で定義し、`await` で呼ぶ**。実案件へ転用する際、リポジトリの実装だけを Supabase 呼び出しに差し替えれば、UI層は一行も変更せずに済む。この構造自体が、営業時の説明材料になる。

```ts
// lib/repo/jobs.ts — 中身は同期でも async にする
export async function findJobs(query: JobQuery): Promise<JobSearchResult> {
  const jobs = useJobStore.getState().jobs
  return filterAndSort(jobs, query)   // 将来ここが supabase.from('jobs')... に変わるだけ
}
```

## 永続化の範囲

| 対象 | 挙動 |
|---|---|
| シードデータ（求人・企業・候補者） | 静的。読み込み専用 |
| ユーザーの操作結果（応募・スカウト・メッセージ・ステータス変更・プロフィール編集） | localStorage に保存。**リロードしても保持される** |
| ファイル添付 | セッション内のみ（Object URL）。リロードでファイル名のみ残る |
| リセット | ヘッダーの「デモをリセット」で localStorage をクリアし初期状態へ |

**擬似的な非同期を必ず入れること。** リポジトリの各操作に 200〜600ms のディレイを挟み、ローディング状態（スケルトン）を実際に見せる。即座に返ると、かえって作り物に見える。

---

# 3. スコープ定義

## 求職者向け機能

| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-C01 | 求人検索 | フリーワード + ファセット絞り込み（職種／勤務地／雇用形態／年収／こだわり条件） | Must |
| F-C02 | 求人詳細 | 労働条件の全項目表示、企業情報、マッチ度、類似求人 | Must |
| F-C03 | プロフィール／Web履歴書 | 基本情報、職務経歴、学歴、資格、スキルタグ、自己PR。完成度％表示 | Must |
| F-C04 | 応募 | 内容確認、志望動機入力、完了通知 | Must |
| F-C05 | 応募管理 | 応募履歴一覧、選考ステータスの可視化、辞退 | Must |
| F-C06 | メッセージ | 企業とのスレッド型チャット、既読、ファイル添付 | Must |
| F-C07 | スカウト受信 | スカウト一覧、興味あり／辞退、返信でメッセージ開始 | Must |
| F-C08 | お気に入り・保存検索 | 求人保存、検索条件保存、マッチ求人の表示 | Should |
| F-C09 | 公開設定 | スカウト受付ON/OFF、特定企業のブロック（現職企業対策） | Should |
| F-C10 | 面接日程調整 | 企業提示の候補日から選択、確定でics発行 | Should |
| F-C11 | 通知センター | 種別フィルタ、既読管理 | Must |

## 企業向け機能

| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-E01 | 企業ダッシュボード | 本日の応募、未読メッセージ、掲載中求人サマリ、要対応アラート | Must |
| F-E02 | 求人作成・編集 | 6ステップウィザード。下書き保存、プレビュー、掲載申請、掲載期限設定 | Must |
| F-E03 | 求人一覧 | ステータス別タブ、複製、掲載期限アラート | Must |
| F-E04 | 応募者管理（ATS） | 選考ステータスのカンバンボード、D&D、担当者アサイン、評価、社内メモ | Must |
| F-E05 | 候補者検索 | スキル・経験年数・希望条件での検索。氏名等は非公開のまま表示 | Must |
| F-E06 | スカウト送信 | テンプレート差し込み、残枠管理 | Must |
| F-E07 | メッセージ | 候補者とのスレッド、定型文、担当者間の内部メモ | Must |
| F-E08 | 面接日程調整 | 候補日の複数提示、確定通知 | Should |
| F-E09 | 採用分析ダッシュボード | 求人別ファネル、スカウト返信率、期間比較 | Should |
| F-E10 | 企業ページ編集 | ロゴ、カバー画像、事業紹介、社員の声、福利厚生 | Should |
| F-E11 | メンバー管理 | メンバー一覧、権限表示・変更 | Could |
| F-E12 | プラン表示 | 掲載プランの比較と選択UI（決済は完了画面まで再現） | Could |

## 運営向け機能

| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-A01 | 求人審査 | NGワード自動検出、目視確認、承認／差戻し（理由付き） | Must |
| F-A02 | 運営ダッシュボード | 審査待ち件数、通報件数、KPI推移グラフ | Should |
| F-A03 | 通報対応 | 通報一覧、対象確認、非公開化 | Should |
| F-A04 | マスタ管理 | 職種、こだわり条件の一覧・並び替え | Could |

## 共通・基盤

| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-S01 | ロール切替 | 求職者／企業／運営をワンクリックで切り替え。切替バーは常時表示 | Must |
| F-S02 | 通知配信 | アプリ内通知センターへの配信。メール送信はプレビュー表示で再現 | Must |
| F-S03 | デモリセット | localStorage をクリアして初期状態に戻す | Must |
| F-S04 | ガイドツアー | 初回訪問時に「何を試せるか」を3ステップで案内 | Should |

## 対象外（Won't have）

| 項目 | 理由 |
|---|---|
| データベース・バックエンドAPI | 2章の方針に基づく |
| 本物の認証（OTP・OAuth・パスワード） | ロール切替で代替 |
| 実際のメール送信・決済処理 | UI上での再現に留める |
| ネイティブアプリ | レスポンシブWebで対応 |
| AIによる求人推薦 | ルールベースのマッチ度％表示で代替 |
| Web面接（ビデオ通話） | 対象外 |
| 多言語UI | 日本語のみ |
| 複数ブラウザ間でのデータ共有 | 各ブラウザで独立。デモとして問題ない |

> **スコープリスク：** 「AIによる求人推薦」は要望として挙がりやすいが、精度の合意形成に時間がかかりスコープが膨張しやすい。**スキルタグの一致数によるマッチ度％表示**という説明可能なルールで実装し、体験としての「賢さ」を担保する。

---

# 4. ロールとデモ切替

## ロール定義

| ロールID | 名称 | 見える画面 |
|---|---|---|
| `guest` | ゲスト（未ログイン想定） | 公開画面のみ（トップ・検索・求人詳細・企業ページ） |
| `candidate` | 求職者 | 公開画面 + マイページ一式 |
| `company` | 企業採用担当 | 企業管理画面一式 |
| `ops` | 運営 | 運営管理画面一式 |

認証は行わない。ロールはストアの状態として保持し、切替バーから即座に変更できる。

## デモ切替バー（F-S01）

画面上部に常時表示する固定バー。以下を持つ。

- 現在のロール表示と切替（4ロール）
- 求職者ロール時：ペルソナ選択（例「佐藤 遥（Webディレクター・31歳）」）
- 企業ロール時：所属企業の選択（シードの30社から）
- 「デモをリセット」ボタン（確認ダイアログ付き）
- 「これはデモです」の明示

**デザイン上の扱い：** このバーはプロダクト本体のUIではない。**プロダクトのデザイントークンとは意図的に異なる扱い**（ダークな帯 + 小さめのタイポ）にして、「アプリの外側にある操作パネル」であることを視覚的に区別する。プロダクト内に紛れ込ませない。

## ロールを跨ぐ体験（デモの肝）

以下が繋がっていることが、このデモの説得力そのもの。**必ず動くようにする。**

1. 求職者として求人に応募 → 企業に切替 → **カンバンの「新規応募」にそのカードが増えている**
2. 企業として候補者にスカウト送信 → 求職者に切替 → **スカウト一覧に届いている**
3. 求職者が「興味あり」 → 企業に切替 → **メッセージスレッドが生成され、氏名が開示されている**
4. 企業が求人を掲載申請 → 運営に切替 → **審査待ちに並んでいる** → 承認 → 求職者側の検索結果に出る

---

# 5. 画面一覧とユーザーフロー

全34画面。画面IDはディレクトリ構成と 1:1 で対応させる。

## 公開・求職者画面

| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-001 | トップページ | `/` | 検索バー、人気職種、注目求人、新着求人、サービス紹介 |
| SC-002 | 求人検索結果 | `/jobs` | ファセットサイドバー、並び替え、求人カード一覧、ページネーション、条件保存 |
| SC-003 | 求人詳細 | `/jobs/[id]` | 労働条件テーブル、企業情報、マッチ度、応募CTA（追従）、類似求人 |
| SC-004 | 企業ページ | `/companies/[id]` | 企業概要、掲載中求人、社員の声、福利厚生 |
| SC-005 | 職種別一覧 | `/jobs/category/[slug]` | 職種解説 + 該当求人一覧 |
| SC-006 | エリア別一覧 | `/jobs/area/[pref]` | 都道府県別求人一覧 |
| SC-010 | 応募フォーム | `/jobs/[id]/apply` | 3ステップ（内容確認→志望動機→最終確認）、離脱防止ダイアログ |
| SC-020 | マイページ | `/mypage` | プロフィール完成度、未読スカウト、選考中一覧、おすすめ求人 |
| SC-021 | プロフィール編集 | `/mypage/profile` | セクション別編集、自動保存、完成度メーター |
| SC-022 | 職務経歴編集 | `/mypage/careers` | 職歴の追加・並び替え・削除、在職中フラグ |
| SC-023 | 応募履歴 | `/mypage/applications` | ステータス別タブ、選考ステップ表示、辞退 |
| SC-024 | スカウト一覧 | `/mypage/scouts` | 未読／興味あり／辞退のタブ |
| SC-025 | メッセージ一覧 | `/mypage/messages` | スレッド一覧、未読バッジ |
| SC-026 | メッセージ詳細 | `/mypage/messages/[id]` | チャットUI、添付、日程調整カード、定型返信 |
| SC-027 | お気に入り | `/mypage/favorites` | 保存求人、保存検索条件 |
| SC-028 | 通知センター | `/mypage/notifications` | 種別フィルタ、既読管理 |
| SC-029 | 各種設定 | `/mypage/settings` | 通知設定、スカウト受付、ブロック企業 |

## 企業管理画面

| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-100 | 企業ダッシュボード | `/admin` | 本日の応募、未読メッセージ、掲載中求人サマリ、要対応アラート |
| SC-101 | 企業ページ編集 | `/admin/company` | ロゴ・カバー画像、リッチテキスト、プレビュー |
| SC-102 | メンバー管理 | `/admin/members` | メンバー一覧、権限変更 |
| SC-110 | 求人一覧 | `/admin/jobs` | ステータス別タブ、複製、掲載期限アラート |
| SC-111 | 求人作成ウィザード | `/admin/jobs/new` | 6ステップ、自動下書き保存、必須項目ガイド、プレビュー |
| SC-112 | 求人編集 | `/admin/jobs/[id]` | 作成と同UI。公開中の編集は再審査対象を明示 |
| SC-120 | 応募者カンバン | `/admin/applicants` | 6列、D&D、担当者アバター、絞り込み |
| SC-121 | 応募者詳細 | `/admin/applicants/[id]` | 履歴書ビュー、5段階評価、社内メモ、ステータス操作 |
| SC-130 | 候補者検索 | `/admin/search` | スキル・経験年数・希望年収での絞り込み、匿名カード一覧 |
| SC-131 | 候補者詳細（匿名） | `/admin/search/[id]` | 匿名プロフィール、スカウト送信ボタン |
| SC-132 | スカウト作成 | `/admin/scouts/new` | テンプレート選択、差し込み変数、対象求人紐付け、残枠表示 |
| SC-133 | スカウト管理 | `/admin/scouts` | 送信履歴、開封率・返信率、テンプレート管理 |
| SC-140 | メッセージ | `/admin/messages` | 2ペイン、定型文、内部メモ |
| SC-150 | 分析ダッシュボード | `/admin/analytics` | 期間選択、求人別ファネル、スカウト効果 |
| SC-160 | プラン | `/admin/plan` | プラン比較、選択、完了画面（決済処理なし） |

## 運営管理画面

| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-200 | 運営ダッシュボード | `/ops` | 審査待ち件数、通報件数、KPI推移グラフ |
| SC-201 | 求人審査 | `/ops/jobs` | NG検出のハイライト、公開プレビュー、承認／差戻し |
| SC-202 | 通報管理 | `/ops/reports` | 通報一覧、対象コンテンツ、非公開化 |
| SC-203 | マスタ管理 | `/ops/masters` | 職種／こだわり条件の一覧・並び替え |

## 主要フロー

### フローA：求職者の応募

```
SC-001 トップ or 検索エンジン流入想定
   ↓
SC-002 求人検索 ── ファセットで絞り込み
   ↓
SC-003 求人詳細 ── 「応募する」
   ↓
SC-010 応募フォーム
   ├ Step1 送信内容の確認（プロフィール差分の編集可）
   ├ Step2 志望動機の入力
   └ Step3 最終確認 → 送信
   ↓
応募完了 ─┬→ 通知センターに「応募を受け付けました」
          ├→ 企業側の通知・ダッシュボード・カンバンに反映
          └→ SC-023 応募履歴に「応募済み」で追加

【体験の確認ポイント】ロール切替バーで企業に切り替えると、
カンバンの「新規応募」列に、今応募したカードが増えている。
```

### フローB：企業のスカウト

```
SC-130 候補者検索 ── スキル「React」× 経験3年以上 × 東京
   ↓
SC-131 候補者詳細（匿名）── 氏名・現職企業は非表示
   ↓
SC-132 スカウト作成 ── テンプレ選択 + {{職種}}{{経験年数}}差し込み → 送信（残枠 -1）
   ↓
【ロール切替：求職者へ】
SC-024 スカウト一覧に着信 ─┬→「辞退」で終了
                          └→「興味あり」
   ↓
メッセージスレッド自動生成（この時点で企業側に氏名・連絡先が開示される）
   ↓
SC-026 / SC-140 メッセージ ── 企業が日程候補を3つ提示（日程調整カード）
   ↓
求職者が候補日を選択 → 確定 ─┬→ 双方の通知センターに配信 + ics ダウンロード
                             └→ SC-120 カンバンが「面接調整中」→「面接設定済」へ自動遷移
```

### フローC：求人の掲載

```
SC-111 求人作成ウィザード
   Step1 基本情報（職種・雇用形態・タイトル）
   Step2 仕事内容（業務内容／変更の範囲）
   Step3 応募資格（必須・歓迎スキル）
   Step4 労働条件（勤務地／変更の範囲・時間・給与・休日・保険・受動喫煙対策）
   Step5 企業アピール（画像・社員の声）
   Step6 掲載設定（掲載期間・応募設定・担当者）
   ↓  ※各ステップで30秒ごと自動下書き保存
プレビュー（求職者から見える実画面）→ 掲載申請
   ↓
【ロール切替：運営へ】
SC-201 求人審査 ── NGワード自動検出（年齢制限／性別限定／必須項目の欠落）
   ├ 承認 → published → 求職者側の検索結果に出現
   └ 差戻し → 理由付きで企業の通知へ → SC-112 で修正 → 再申請
```

---

# 6. 機能要件

各要件は判定可能な粒度で記述している。テスト項目と 1:1 で対応させる。

## 6.1 ロール・デモ基盤

| ID | 要件 | 優先 |
|---|---|---|
| FR-001 | 画面上部の固定バーから、4ロール（ゲスト／求職者／企業／運営）をワンクリックで切り替えられること。切替時に該当ロールのトップ画面へ遷移すること | P1 |
| FR-002 | 求職者ロールでは3名のペルソナから、企業ロールでは所属企業を選択できること | P1 |
| FR-003 | 「デモをリセット」で localStorage をクリアし、確認ダイアログを経て初期状態に戻ること | P1 |
| FR-004 | 操作結果はブラウザをリロードしても保持されること | P1 |
| FR-005 | 初回訪問時に3ステップのガイドツアーを表示し、「試せること」を案内すること。「今後表示しない」を選択できること | P2 |
| FR-006 | リポジトリ層の各操作は 200〜600ms の擬似ディレイを持ち、その間スケルトンを表示すること | P1 |
| FR-007 | ロールがアクセスできない画面（例：ゲストで `/admin`）にアクセスした場合、専用の案内画面から適切なロールへ切り替えられること。素の404を出さないこと | P1 |

## 6.2 求人検索

| ID | 要件 | 優先 |
|---|---|---|
| FR-101 | フリーワード検索は、求人タイトル・業務内容・企業名・スキルタグを対象とすること。ひらがな／カタカナ／半角全角の表記ゆれを吸収すること | P1 |
| FR-102 | ファセット絞り込みとして、職種（大分類→中分類）、勤務地（都道府県→市区町村）、雇用形態、年収レンジ、こだわり条件（リモート可／未経験歓迎／土日休み等）を提供すること | P1 |
| FR-103 | 各ファセット項目の横に、現在の絞り込み条件下でのヒット件数を表示すること。0件の項目は選択不可にすること | P1 |
| FR-104 | 絞り込み条件はURLクエリパラメータに反映し、ブラウザバック・URL共有・リロードで同一結果を再現できること | P1 |
| FR-105 | 並び替えは「新着順」「マッチ度順（求職者ロール時のみ）」「年収が高い順」を提供すること | P1 |
| FR-106 | 検索結果0件時、条件を1つ緩和した場合の件数を提示し、ワンタップで適用できること（例：「勤務地を『東京都』に広げると 124件」） | P2 |
| FR-107 | 求職者ロール時、各求人カードにマッチ度（％）を表示すること。算出は「希望条件との一致項目数 ÷ 評価項目数」とし、ホバー／タップで内訳を開示すること | P2 |
| FR-108 | 検索条件に名前を付けて保存でき、保存条件に合致する求人件数を SC-027 で確認できること | P2 |
| FR-109 | 検索結果は20件ずつ表示し、モバイルは無限スクロール、デスクトップはページネーションとすること | P1 |
| FR-110 | 適用中の絞り込み条件をチップで一覧表示し、個別に解除できること | P1 |

## 6.3 求人詳細・応募

| ID | 要件 | 優先 |
|---|---|---|
| FR-201 | 求人詳細に、労働条件の全項目を構造化テーブルで表示すること（7章の Job 型の必須項目すべて） | P1 |
| FR-202 | モバイルでは応募CTAを画面下部に固定表示し、スクロール位置に関わらず常に応募できること | P1 |
| FR-203 | ゲストロールで応募を試みた場合、求職者ロールへの切替を促し、切替後に元の応募フォームへ復帰すること | P1 |
| FR-204 | 応募フォームは3ステップで構成し、各ステップの入力内容を保持して、ブラウザバックでも消失しないこと | P1 |
| FR-205 | 入力途中でページを離脱しようとした場合、確認ダイアログを表示すること | P2 |
| FR-206 | 同一求人への重複応募を禁止し、応募済みの場合は応募ボタンを「応募済み」表示に置き換えること | P1 |
| FR-207 | 応募完了時、求職者の通知センターと企業側の通知・ダッシュボード・カンバンに即座に反映すること | P1 |
| FR-208 | 求職者は選考結果が出る前であれば応募を辞退でき、理由（任意）を送信できること | P2 |
| FR-209 | 求人詳細下部に、同一職種・同一エリアの類似求人を最大6件表示すること | P2 |
| FR-210 | 掲載終了した求人のURLは「この求人の募集は終了しました」+ 類似求人を表示すること | P2 |

## 6.4 プロフィール・Web履歴書

| ID | 要件 | 優先 |
|---|---|---|
| FR-301 | プロフィールは「基本情報／職務経歴／学歴／資格・スキル／希望条件／自己PR」の6セクションで構成し、セクション単位で編集・保存できること | P1 |
| FR-302 | 入力状況に応じたプロフィール完成度（％）を常時表示し、未入力セクションへの導線を示すこと | P1 |
| FR-303 | 職務経歴は複数登録でき、ドラッグで並び替えられること。「現在も在籍中」を選択した場合、終了年月の入力を無効化すること | P1 |
| FR-304 | スキルはタグ形式で登録し、入力時にマスタからのサジェストを表示すること。マスタにない場合もフリー入力を許可すること | P1 |
| FR-305 | スカウト受付を ON/OFF でき、OFF時は候補者検索の結果に表示されないこと | P1 |
| FR-306 | 特定の企業を指定してブロックでき、ブロックした企業の候補者検索結果には表示されないこと（現職企業への露出防止） | P1 |
| FR-307 | プロフィール編集は入力停止から2秒後に自動保存し、保存状態を画面上で示すこと | P2 |

## 6.5 スカウト・メッセージ

| ID | 要件 | 優先 |
|---|---|---|
| FR-401 | 候補者検索の結果および候補者詳細では、氏名・メールアドレス・電話番号・現職企業名・生年月日を表示しないこと。年齢は年代（30代等）で表示すること | P1 |
| FR-402 | **候補者検索の絞り込み条件に、性別・年齢・国籍を含めないこと** | P1 |
| FR-403 | スカウト本文にテンプレートを使用でき、`{{職種}}` `{{経験年数}}` `{{スキル}}` の差し込み変数を候補者データで置換できること | P1 |
| FR-404 | スカウト送信時、企業プランの残枠を検証し、枠不足時は送信をブロックしてプラン画面への導線を表示すること | P1 |
| FR-405 | 同一候補者への同一企業からのスカウトは、前回送信から30日間は再送不可とすること | P2 |
| FR-406 | 求職者が「興味あり」を選択した時点で、当該企業に対してのみ氏名・連絡先を開示すること | P1 |
| FR-407 | メッセージ送信はストア更新と同時に相手ロールのビューへ反映されること（ロール切替後に確認できること） | P1 |
| FR-408 | 既読状態を相手に表示すること。求職者側は既読表示のOFF設定を選べること | P2 |
| FR-409 | メッセージにファイル（PDF／画像、1ファイル10MBまで、1メッセージ3件まで）を添付できること。セッション内で開けること | P2 |
| FR-410 | 企業側は同一スレッド内に、求職者に見えない内部メモを残せること。内部メモは背景色とラベルで明確に区別すること | P2 |
| FR-411 | 企業が最大5つの候補日時を提示し、求職者が1つを選択して確定できる日程調整カードをメッセージ内に挿入できること | P2 |
| FR-412 | 日程確定時、双方の通知センターに配信し、ics形式のカレンダーファイルをダウンロードできること | P2 |
| FR-413 | メッセージ本文中の電話番号・メールアドレスらしき文字列を検出し、送信前に注意を表示すること | P3 |

## 6.6 企業側 求人・応募者管理

| ID | 要件 | 優先 |
|---|---|---|
| FR-501 | 求人作成は6ステップのウィザード形式とし、30秒ごとに下書きを自動保存すること | P1 |
| FR-502 | 労働条件の必須項目（7章 Job 型の required 項目）が未入力の場合、掲載申請できないこと。どの項目が不足しているかをステップ単位で示すこと | P1 |
| FR-503 | 固定残業代を「あり」にした場合、内訳と相当時間数の入力を必須化すること | P1 |
| FR-504 | 有期雇用を選択した場合、契約期間と更新上限の入力を必須化すること | P1 |
| FR-505 | 掲載申請前に、求職者から見える実際の画面をプレビューできること | P1 |
| FR-506 | 既存求人を複製して新規作成でき、複製時はステータスを draft とすること | P1 |
| FR-507 | 掲載期限が7日以内に迫った求人を、一覧とダッシュボードでアラート表示すること | P2 |
| FR-508 | 応募者管理はカンバン形式とし、列は「新規応募／書類選考中／面接調整中／面接設定済／内定／見送り」の6段階とすること | P1 |
| FR-509 | カードのドラッグ＆ドロップでステータスを変更でき、変更が即座に反映されること | P1 |
| FR-510 | **カンバンのステータス変更をキーボードのみでも行えること**（カード選択 → 移動先選択） | P1 |
| FR-511 | 「見送り」へ移動した際、不採用通知の送信有無を選択でき、テンプレートを編集できること。送信するとメールプレビューが表示され、求職者側のステータスに反映されること | P1 |
| FR-512 | 応募者ごとに担当者をアサインでき、担当者で絞り込めること | P2 |
| FR-513 | 応募者詳細で5段階評価とコメントを記録でき、複数メンバーの評価を一覧できること | P2 |
| FR-514 | 分析ダッシュボードで、求人別の「表示数→応募数→書類通過→面接→内定」のファネルを表示し、期間を指定できること | P2 |
| FR-515 | 応募時点のプロフィールを凍結して保持し、応募後にプロフィールを編集しても、企業側に表示される応募内容が変わらないこと | P1 |

## 6.7 運営・審査

| ID | 要件 | 優先 |
|---|---|---|
| FR-601 | 求人審査時、以下を自動検出してハイライト表示すること：年齢制限を示す表現／性別を限定する表現／必須項目の欠落／給与の曖昧な表記 | P1 |
| FR-602 | 差戻し理由は定型選択（複数選択可）+ 自由記述で入力し、企業側の通知に反映すること | P1 |
| FR-603 | 審査は一覧画面から遷移せずにモーダルで完結でき、承認後は次の審査対象へ自動送りできること | P2 |
| FR-604 | 承認された求人が、求職者側の検索結果に即座に出現すること | P1 |
| FR-605 | 求人・企業ページに対する通報を受け付け、運営が対応状況（未対応／対応中／対応済）を管理できること | P2 |
| FR-606 | 運営ダッシュボードで、会員数・求人数・応募数・スカウト数の推移をグラフ表示すること | P2 |

## 6.8 通知

| ID | 要件 | 優先 |
|---|---|---|
| FR-701 | 通知センターに、応募受付・応募着信・スカウト受信・新着メッセージ・選考ステータス変更・日程確定・審査結果 の各種別が配信されること | P1 |
| FR-702 | 未読件数をヘッダーのバッジに表示し、種別でフィルタできること | P1 |
| FR-703 | 設定画面で通知種別ごとに ON/OFF でき、OFF にした種別は配信されないこと | P2 |
| FR-704 | メール送信を伴う操作（応募受付・不採用通知等）では、**実際に送られるメールのプレビュー**をモーダルで表示すること。デモであることを明示しつつ、実装の完成度を示す | P2 |

---

# 7. データ設計

## 型定義（`lib/types/`）

すべての型を先に定義し、これを唯一の真実とする。シードもストアもコンポーネントもこの型を参照する。

```ts
// ---- マスタ ----
type JobCategory = { id: string; parentId: string | null; name: string; slug: string; sortOrder: number }
type Prefecture  = { code: string; name: string; region: string }
type City        = { code: string; prefCode: string; name: string }
type JobFeature  = { id: string; name: string; icon: string }   // リモート可, 未経験歓迎 等
type Skill       = { id: string; name: string; categoryId: string }

// ---- 企業 ----
type Company = {
  id: string
  name: string
  logoUrl: string
  coverUrl: string
  industry: string
  employeeCount: number
  foundedYear: number
  address: string
  description: string          // リッチテキスト（サニタイズ済みHTML）
  benefits: string[]
  voices: { name: string; role: string; avatarUrl: string; body: string }[]
  planId: 'light' | 'standard' | 'premium'
  scoutQuota: number           // 月間スカウト枠
  scoutUsed: number
  members: CompanyMember[]
}
type CompanyMember = { id: string; name: string; avatarUrl: string; role: 'admin' | 'recruiter' | 'viewer' }

// ---- 求人 ----
type Job = {
  id: string
  companyId: string
  title: string                        // 60字まで
  categoryId: string
  employmentType: '正社員' | '契約社員' | 'パート・アルバイト' | '業務委託'
  // --- 労働条件（すべて必須。掲載申請のバリデーション対象） ---
  description: string                  // 業務内容（50字以上）
  descriptionScope: string             // 業務の変更の範囲
  workLocation: string                 // 就業場所
  workLocationScope: string            // 就業場所の変更の範囲
  prefCode: string
  cityCode: string
  salaryType: '年俸' | '月給' | '時給'
  salaryMin: number
  salaryMax: number                    // 上限も必須。片側のみは不可
  fixedOvertime: { enabled: boolean; amount?: number; hours?: number }  // enabled時は内訳必須
  workHours: string                    // 始業・終業・休憩・所定外労働の有無
  holidays: string
  insurances: ('健康保険' | '厚生年金' | '雇用保険' | '労災保険')[]
  smokingPolicy: string                // 受動喫煙防止措置
  contractPeriod: string               // 有期の場合は更新上限を含む。無期なら「期間の定めなし」
  trialPeriod: string                  // 試用期間の有無・期間・条件
  // --- 応募資格 ---
  requiredSkills: string[]
  welcomeSkills: string[]
  features: string[]                   // JobFeature.id[]
  // --- 掲載管理 ---
  status: 'draft' | 'under_review' | 'published' | 'rejected' | 'closed' | 'expired'
  rejectReason?: { codes: string[]; note: string }
  publishedAt?: string
  expiresAt?: string
  viewCount: number
  assigneeId?: string                  // 企業側担当者
  createdAt: string
  updatedAt: string
}

// ---- 候補者 ----
type Candidate = {
  id: string
  // 匿名ビューから除外される項目
  lastName: string; firstName: string
  lastNameKana: string; firstNameKana: string
  email: string
  phone: string
  birthDate: string
  currentCompanyName: string
  // 公開可能な項目
  avatarUrl: string
  prefCode: string
  currentCategoryId: string
  experienceYears: number
  currentSalary?: number
  desiredSalaryMin?: number
  desiredPrefCodes: string[]
  desiredEmploymentTypes: string[]
  selfPr: string                       // 3000字まで
  careers: Career[]
  educations: Education[]
  skills: { skillId: string; level: 1|2|3|4|5 }[]
  certifications: { name: string; acquiredAt: string }[]
  scoutEnabled: boolean
  blockedCompanyIds: string[]
  profileCompletion: number            // 算出値
}
type Career    = { id: string; companyName: string; role: string; from: string; to: string | null; isCurrent: boolean; body: string; sortOrder: number }
type Education = { id: string; schoolName: string; faculty: string; from: string; to: string }

// 候補者検索で使う匿名型。Candidate から機械的に導出する
type AnonymousCandidate = Omit<Candidate,
  'lastName'|'firstName'|'lastNameKana'|'firstNameKana'|'email'|'phone'|'birthDate'|'currentCompanyName'|'careers'
> & {
  ageDecade: 20 | 30 | 40 | 50          // 年代のみ
  careers: Omit<Career, 'companyName'>[] // 企業名を伏せた職歴
}

// ---- 応募・スカウト・メッセージ ----
type Application = {
  id: string
  jobId: string
  candidateId: string
  status: 'new' | 'screening' | 'scheduling' | 'scheduled' | 'offer' | 'rejected' | 'withdrawn'
  motivation: string
  profileSnapshot: Candidate           // 応募時点のプロフィールを凍結（FR-515）
  assigneeId?: string
  evaluations: { memberId: string; score: 1|2|3|4|5; comment: string; createdAt: string }[]
  internalNotes: { memberId: string; body: string; createdAt: string }[]
  threadId: string
  appliedAt: string
}

type Scout = {
  id: string
  companyId: string
  candidateId: string
  jobId: string
  body: string
  status: 'sent' | 'opened' | 'interested' | 'declined'
  threadId?: string                    // interested になった時点で生成
  sentAt: string
  openedAt?: string
}

type Thread = {
  id: string
  type: 'application' | 'scout'
  companyId: string
  candidateId: string
  jobId: string
  messages: Message[]
  lastReadByCandidate?: string
  lastReadByCompany?: string
}
type Message = {
  id: string
  senderType: 'candidate' | 'company' | 'system'
  senderId: string
  body: string
  attachments: { name: string; size: number; objectUrl: string }[]
  isInternalMemo: boolean              // trueなら求職者に表示しない
  interviewSlot?: InterviewSlot
  createdAt: string
}
type InterviewSlot = {
  id: string
  proposedAt: string[]                 // 最大5件
  selectedAt?: string
  status: 'proposed' | 'confirmed' | 'declined'
}

// ---- その他 ----
type Notification = {
  id: string
  targetRole: 'candidate' | 'company' | 'ops'
  targetId: string
  type: 'application_received'|'application_submitted'|'scout_received'|'message'|'status_changed'|'interview_confirmed'|'review_result'
  title: string
  body: string
  link: string
  emailPreview?: { subject: string; html: string }   // FR-704
  readAt?: string
  createdAt: string
}
type SavedSearch = { id: string; candidateId: string; name: string; query: JobQuery; createdAt: string }
type Report      = { id: string; targetType: 'job'|'company'; targetId: string; reason: string; status: 'open'|'in_progress'|'closed'; createdAt: string }
```

## シードデータ（`lib/seed/`）

| ファイル | 内容 | 件数 |
|---|---|---|
| `masters.ts` | 職種（大分類8 → 中分類60）、都道府県47、主要市区町村、こだわり条件20、スキル200 | — |
| `companies.ts` | 架空企業 | 30社 |
| `jobs.ts` | 求人（published 260 / draft 20 / under_review 10 / expired 10） | 300件 |
| `candidates.ts` | 候補者（うち3名を操作用ペルソナとして詳細に作り込む） | 200名 |
| `applications.ts` | 既存応募（各ステータスに分散配置） | 60件 |
| `scouts.ts` | 既存スカウト（各ステータスに分散） | 40件 |
| `threads.ts` | 会話履歴（自然な往復3〜10通） | 30件 |
| `stats.ts` | 分析ダッシュボード用の日次データ（過去90日分） | — |

**シード作成のルール（品質を左右する）**

- **実在企業名・実在人名を使わない。** 架空のものを使う
- 画像はライセンス確認済みの素材のみ。人物写真は取得元を記録する
- 求人票の本文は**テンプレの繰り返しにしない。** 職種ごとに書き分ける。ここが手抜きだとデモ全体が安っぽくなる
- 年収・勤務地・スキルの分布に偏りを持たせる。均等分布は不自然
- 日付は現在日時からの相対で生成する（`3日前`『2週間前』等）。固定日付を埋め込むと、時間が経つと古びる
- 応募・スカウトのステータスは全種類を含める。**どの画面を開いても空にしない**

## ストアとリポジトリ

```
lib/
├── types/          # 上記の型定義
├── seed/           # シードデータ
├── store/          # Zustand + persist(localStorage)
│   ├── session.ts  # 現在のロール、ペルソナ、企業
│   ├── data.ts     # jobs / candidates / applications / scouts / threads / notifications
│   └── ui.ts       # ガイドツアーの表示済みフラグ等
└── repo/           # ★ コンポーネントが触るのはここだけ
    ├── jobs.ts
    ├── candidates.ts
    ├── applications.ts
    ├── scouts.ts
    ├── threads.ts
    └── notifications.ts
```

**リポジトリ層の規約（厳守）**

- 全メソッドを `async` で定義する。中身が同期でも例外なく
- 200〜600ms の擬似ディレイを挟む（`lib/repo/_delay.ts` に共通実装）
- 戻り値は `{ ok: true; data: T } | { ok: false; error: string }` に統一する
- **コンポーネントから Zustand ストアを直接参照しない。** 読み取りも書き込みもリポジトリ経由
- 匿名化（`Candidate` → `AnonymousCandidate`）は `repo/candidates.ts` の内部で行う。**コンポーネントに `Candidate` 型を渡さない**（FR-401）

```ts
// lib/repo/candidates.ts
export async function searchCandidates(q: CandidateQuery): Promise<Result<AnonymousCandidate[]>> {
  await delay()
  const { candidates } = useDataStore.getState()
  const companyId = useSessionStore.getState().companyId
  const visible = candidates.filter(c =>
    c.scoutEnabled && !c.blockedCompanyIds.includes(companyId)   // FR-305, FR-306
  )
  return { ok: true, data: filterAndSort(visible, q).map(toAnonymous) }  // 匿名化はここで
}
```

---

# 8. デザイン要件

## アートディレクション

**「書類の重さを、紙のような静けさで扱う」**

求人・履歴書・選考は本質的に「書類」の世界。多くの求人サイトは情報量に負けて、彩度の高いバッジとカードが乱立する。HIREBASE は逆に、**余白・罫線・タイポグラフィの階層だけで情報を整理し、色は意味のあるところにしか使わない**。アクセントカラーは「今あなたが取れる行動」にのみ与える。

## カラートークン

`app/globals.css` に CSS 変数で定義し、`tailwind.config.ts` の theme から参照する。**直書きの色指定を禁止する。**

```css
:root {
  --ink-900: #131A24;   /* 見出し・本文 */
  --ink-600: #4A5566;   /* 補助テキスト */
  --ink-400: #8B94A3;   /* ラベル・非活性 */
  --brand-700: #1B4D3E; /* 主要CTA・ブランド */
  --brand-500: #2E7D64; /* ホバー・アクティブ */
  --brand-50:  #E8F0EC; /* 選択状態の背景 */
  --accent:    #C2703B; /* スカウト・注目のみ */
  --surface:   #FBFAF8; /* ページ背景 */
  --card:      #FFFFFF;
  --border:    #E6E3DC;
  --success: #2E7D64; --warning: #B4741C; --danger: #A83A3A; --info: #33608F;
}
```

**配色ルール：** アクセントカラー `--accent` は**1画面につき最大2箇所まで**。「スカウトが届いている」「応募する」など、ユーザーが今取るべき行動にのみ使う。装飾目的の使用を禁止する。

## タイポグラフィ

| 役割 | 書体 | 用途 |
|---|---|---|
| ディスプレイ | Zen Old Mincho 700 | ページタイトル、求人タイトル、企業名。**明朝体を主役に据えるのが視覚的差別化点** |
| 本文 | Noto Sans JP 400/500 | 説明文、フォームラベル、UI全般 |
| 数値・ID | Roboto Mono 500 | 年収、応募数、日付、求人ID |

| トークン | デスクトップ | モバイル | 行間 | 用途 |
|---|---|---|---|---|
| display | 40px | 28px | 1.35 | ページタイトル |
| h1 | 28px | 22px | 1.45 | 求人タイトル |
| h2 | 20px | 18px | 1.5 | セクション見出し |
| h3 | 16px | 15px | 1.6 | 小見出し |
| body | 15px | 15px | 1.85 | 本文（モバイルでも縮小しない） |
| sm | 13px | 13px | 1.7 | 補助説明 |
| caption | 11px | 11px | 1.6 | ラベル、注記 |

本文の1行あたり文字数は日本語で **36〜42文字**に収まるようコンテンツ幅を制御する。

## レイアウトとスペーシング

| 項目 | 定義 |
|---|---|
| スペーシング | 4pxベース：4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 / 96 |
| ブレークポイント | sm 640 / md 768 / lg 1024 / xl 1280 / 2xl 1440 |
| コンテンツ最大幅 | 1200px（管理画面は1440px） |
| グリッド | デスクトップ12カラム／タブレット8／モバイル4。ガター24px |
| 角丸 | 6px（カード・入力）／4px（バッジ）／999px（タグ・アバター） |
| 影 | 2段階のみ。`sm: 0 1px 2px rgba(19,26,36,.06)` / `md: 0 4px 16px rgba(19,26,36,.08)` |
| 罫線 | 1px `#E6E3DC`。**影よりも罫線でカードを区切ることを優先する** |

## 主要コンポーネント仕様

| コンポーネント | 仕様 |
|---|---|
| 求人カード | 企業ロゴ40px／求人タイトル（2行クランプ）／企業名／年収（Mono）／勤務地／こだわり条件タグ最大3+「他N件」／マッチ度／保存アイコン |
| ボタン | primary（brand-700塗り）／secondary（枠線）／ghost／danger の4種。高さ40px（sm 32 / lg 48）。**タップ領域は最低44×44px** |
| 入力フィールド | 高さ44px。ラベルは上部固定（プレースホルダをラベル代わりにしない）。エラーはフィールド直下に色+アイコン+文言。必須は「必須」バッジ（※印は使わない） |
| 選考ステータスバッジ | 6状態それぞれに固有色。色のみに依存せずテキストを併記 |
| カンバンカード | 候補者名／応募求人／経過日数／担当者アバター／評価星。ドラッグ中は3度傾け、影を md に引き上げる |
| チャットバブル | 自分＝brand-50背景・右寄せ／相手＝白+罫線・左寄せ／内部メモ＝warning系背景+「社内メモ」ラベル／システム＝中央・グレー小文字 |
| 空状態 | 全一覧画面に専用の空状態。線画イラスト+「何が表示されるか」の説明+次の行動ボタン。**「データがありません」単独表示は不可** |
| スケルトン | データ取得中はスピナーではなくスケルトン。レイアウトシフトを起こさない |
| デモ切替バー | **プロダクトのトークンとは意図的に別扱い**（ダークな帯 + 小さめタイポ）。アプリの外側のパネルであることを視覚的に区別する |

## モーション

| 対象 | duration | 内容 |
|---|---|---|
| ホバー・フォーカス | 120ms | ease-out。色と影のみ。移動は伴わない |
| モーダル・ドロワー | 240ms | cubic-bezier(.16,1,.3,1)。背後は8pxブラー+暗転 |
| カンバンのドラッグ | 180ms | spring風。ドロップ時に着地のバウンド |
| ページ遷移 | 200ms | フェード+8px上方向。検索結果の再取得時は行わない |
| 検索結果の更新 | 150ms | 件数のカウントアップのみ。カードの再アニメーションは行わない |
| トースト | 200ms | 下から出現。5秒で自動消滅（エラーは手動閉じのみ） |

`prefers-reduced-motion: reduce` 時は全アニメーションを無効化する。**スクロール連動アニメーションは使用しない** — 求人検索は目的をもって使う画面であり、演出は操作の妨げになる。

## アクセシビリティ（WCAG 2.1 AA）

| ID | 要件 |
|---|---|
| A11Y-01 | テキストと背景のコントラスト比は通常4.5:1以上、18px以上は3:1以上 |
| A11Y-02 | 全ての操作をキーボードのみで完遂できる。カンバンのドラッグにはキーボード代替を用意（FR-510） |
| A11Y-03 | フォーカスリングは2px・オフセット2pxで常時可視。`outline:none` の単独使用禁止 |
| A11Y-04 | 情報を色のみで伝えない（ステータス = 色 + テキスト） |
| A11Y-05 | 全ての画像に代替テキスト。装飾画像は `alt=""` |
| A11Y-06 | フォームの各入力に `label` を関連付け、エラーは `aria-describedby` と `role="alert"` で読み上げる |
| A11Y-07 | モーダルはフォーカストラップ + Esc で閉じる + 起動元へフォーカス復帰 |
| A11Y-08 | 非同期の結果（検索件数の更新、保存完了）を `aria-live` で通知 |
| A11Y-09 | ページタイトルは画面ごとに固有。見出しレベルを飛ばさない |
| A11Y-10 | ブラウザのフォントサイズ200%指定でも横スクロールを発生させず内容が読める |

## ライティング規約

| 原則 | 例 |
|---|---|
| ボタンは「何が起きるか」 | ○「応募内容を送信」／×「送信」「OK」 |
| 操作名は最後まで変えない | 「掲載を申請」→ トーストも「掲載を申請しました」 |
| エラーは原因と対処を書く | ○「ファイルサイズが10MBを超えています。圧縮するか、別のファイルを選んでください」／×「アップロードに失敗しました」 |
| 謝罪しない | ×「申し訳ございませんが〜」／○ 事実と次の行動 |
| 空状態は誘導する | ○「まだスカウトは届いていません。プロフィールを充実させると、企業の目に留まりやすくなります」+ 編集ボタン |
| システム語を使わない | ○「掲載を終了する」／×「ステータスをclosedに更新」 |

---

# 9. 技術要件・ディレクトリ構成

## 技術スタック（固定・勝手に変更しない）

| レイヤ | 技術 | 備考 |
|---|---|---|
| フレームワーク | Next.js 15（App Router）／ TypeScript strict | |
| スタイリング | Tailwind CSS + CSS Variables | トークンは CSS 変数で定義 |
| UIコンポーネント | shadcn/ui（Radix UI基盤） | a11y要件を自前実装せずに満たす |
| 状態管理 | Zustand + persist ミドルウェア | localStorage に永続化 |
| フォーム | React Hook Form + Zod | |
| D&D | dnd-kit | **キーボード操作対応が必須のため。react-beautiful-dnd は使わない** |
| グラフ | Recharts | 分析ダッシュボード |
| 日付 | date-fns（ja locale） | |
| アイコン | lucide-react | |
| ホスティング | Vercel | |
| テスト | Vitest / Playwright / axe-core | |

**上記以外のライブラリを入れる前に必ず提案し、承認を得ること。**

## ディレクトリ構成

```
/
├── CLAUDE.md                  # このファイル
├── app/
│   ├── layout.tsx             # デモ切替バーをここに配置
│   ├── page.tsx               # SC-001
│   ├── jobs/
│   │   ├── page.tsx           # SC-002
│   │   ├── [id]/page.tsx      # SC-003
│   │   ├── [id]/apply/page.tsx# SC-010
│   │   ├── category/[slug]/page.tsx  # SC-005
│   │   └── area/[pref]/page.tsx      # SC-006
│   ├── companies/[id]/page.tsx# SC-004
│   ├── mypage/                # SC-020〜029
│   ├── admin/                 # SC-100〜160
│   ├── ops/                   # SC-200〜203
│   └── dev/components/        # コンポーネントカタログ（本番では非公開）
├── components/
│   ├── ui/                    # shadcn/ui プリミティブ
│   ├── domain/                # JobCard, ApplicantKanban, ChatThread, ConditionChips 等
│   ├── demo/                  # RoleSwitcher, ResetButton, GuideTour, EmailPreviewModal
│   └── layout/
├── lib/
│   ├── types/  seed/  store/  repo/   # 7章参照
│   ├── search/                # 検索・ファセット・マッチ度のロジック
│   ├── validation/            # Zod スキーマ
│   └── utils/
├── e2e/                       # Playwright
└── public/                    # 画像素材
```

## コーディング規約

- TypeScript strict。`any` 禁止。やむを得ない場合は `unknown` + 型ガード
- ファイル名は kebab-case、コンポーネントは PascalCase
- 1ファイル300行を超えたら分割を検討する
- **コンポーネントから Zustand ストアを直接参照しない。必ず `lib/repo/` 経由**
- 検索・マッチ度・バリデーションなどのロジックは `lib/` に置き、コンポーネントに埋め込まない（テスト対象にするため）
- コミットは1タスクごと。メッセージに要件IDを含める
  例：`feat(jobs): 求人検索のファセット絞り込みを実装 (FR-102, FR-103)`

---

# 10. 非機能要件

| ID | 要件 | 目標値 |
|---|---|---|
| NFR-01 | LCP（モバイル・4G相当） | 2.5秒以下 |
| NFR-02 | INP（操作への応答） | 200ms以下 |
| NFR-03 | CLS（レイアウトシフト） | 0.1以下 |
| NFR-04 | 初期JSバンドルサイズ（公開画面・gzip後） | 200KB以下 |
| NFR-05 | 求人300件に対する検索・絞り込みの体感遅延 | 擬似ディレイを除き即時 |
| NFR-06 | 画像は WebP/AVIF、遅延読み込み | — |
| NFR-07 | Lighthouse | Performance 90 / Accessibility 95 / SEO 95 以上 |
| NFR-08 | 対応ブラウザ | Chrome / Safari / Edge 最新2バージョン、iOS Safari 16以降 |
| NFR-09 | 全ページに固有の title / description / OGP画像 | — |
| NFR-10 | ユーザー入力はサニタイズし、リッチテキストは許可タグのホワイトリスト方式とする | — |

## 実案件への転用時の注意（デモ本体の要件ではない）

将来この構成を実サービスへ転用する場合、以下が追加で必要になる。**LP や商談でこの点に触れられた際に説明できるよう、記録として残す。**

- 求人票は職業安定法上の労働条件明示義務の対象。本デモの `Job` 型は明示必須項目を網羅済み（業務の変更の範囲・就業場所の変更の範囲・受動喫煙防止措置を含む）
- 求職者情報を収集して企業に提供する場合、募集情報等提供事業者としての届出が必要になる可能性がある
- 年齢制限・性別限定の禁止（労働施策総合推進法／均等法）。本デモは候補者検索の条件から性別・年齢・国籍を除外済み（FR-402）
- 個人情報保護法に基づく利用目的の明示、開示・削除請求への対応
- **本デモは個人情報をサーバーに送信しないため、これらの義務は発生しない。** ただし「デモである」旨は画面上に常時明示する

---

# 11. 実装タスク

**この順に進める。フェーズを飛ばさない。** 完了時は `[x]` に更新する。

## Phase 0 — 基盤とデザインシステム（3日）

### 0-1. 初期化
- [ ] Next.js 15 / TypeScript strict / App Router / Tailwind で初期化
- [ ] ESLint / Prettier / husky（pre-commit で lint + typecheck）
- [ ] 9章のディレクトリ構成を作成

### 0-2. 型とシード
- [ ] `lib/types/` に7章の型定義をすべて実装
- [ ] `lib/seed/masters.ts`（職種8→60、都道府県47、市区町村、こだわり条件20、スキル200）
- [ ] `lib/seed/companies.ts`（架空30社。ロゴ・カバー画像含む）
- [ ] `lib/seed/jobs.ts`（300件。**職種ごとに本文を書き分ける。テンプレの使い回し禁止**）
- [ ] `lib/seed/candidates.ts`（200名。うち3名は操作用ペルソナとして詳細に作り込む）
- [ ] `lib/seed/{applications,scouts,threads,stats}.ts`（各ステータスに分散配置）
- [ ] 日付は現在日時からの相対で生成する

### 0-3. ストアとリポジトリ
- [ ] `lib/store/{session,data,ui}.ts` を Zustand + persist で実装
- [ ] `lib/repo/` の全モジュールを実装。**全メソッド async、擬似ディレイ、Result型で統一**
- [ ] `toAnonymous()` を `repo/candidates.ts` 内に実装（FR-401）
- [ ] リポジトリ層の単体テストを書く（この時点で書く。後回しにしない）

### 0-4. デザインシステム
- [ ] `app/globals.css` にカラートークンを CSS 変数で定義
- [ ] `tailwind.config.ts` から CSS 変数を参照するよう theme を拡張（スペーシング・角丸・影・ブレークポイント）
- [ ] フォント（Zen Old Mincho / Noto Sans JP / Roboto Mono）を `next/font` で最適化
- [ ] タイプスケールをユーティリティクラスとして定義
- [ ] shadcn/ui を導入し、トークンに合わせて Button / Input / Select / Checkbox / Radio / Dialog / Sheet / Toast / Badge / Tabs / Tooltip / DropdownMenu を上書き
- [ ] 共通コンポーネント：`EmptyState` / `Skeleton` / `PageHeader` / `Pagination` / `FileUploader` / `TagInput` / `StatusBadge` / `ConditionChips`
- [ ] `prefers-reduced-motion` の全体対応
- [ ] **`/dev/components` にコンポーネントカタログを作成し、全バリアントを一覧できるようにする**

### 0-5. デモ基盤
- [ ] `RoleSwitcher`（4ロール切替、ペルソナ選択、企業選択）(FR-001, FR-002)
- [ ] デモ切替バーを `app/layout.tsx` に固定配置。**プロダクトのトークンとは別扱いのデザイン**
- [ ] 「デモをリセット」（確認ダイアログ + localStorage クリア）(FR-003)
- [ ] ロール権限外アクセス時の案内画面（素の404を出さない）(FR-007)

### Phase 0 完了チェック
- [ ] `/dev/components` で全コンポーネントがトークン通りに表示される
- [ ] ロール切替が動作し、リロードしても状態が保持される
- [ ] リポジトリ層のテストが通る

---

## Phase 1 — 求職者体験（4日）

### 1-1. 求人検索
- [ ] `lib/search/` に検索・ファセット・マッチ度のロジックを実装（テスト付き）
- [ ] 表記ゆれ吸収（ひらがな／カタカナ／半角全角）(FR-101)
- [ ] SC-002 検索結果。URLクエリと状態を完全同期 (FR-104)
- [ ] ファセットサイドバー。ヒット件数表示、0件は選択不可 (FR-102, FR-103)
- [ ] 適用条件のチップ表示と個別解除 (FR-110)
- [ ] 並び替え3種 (FR-105)
- [ ] モバイル無限スクロール／デスクトップページネーション (FR-109)
- [ ] 0件時の条件緩和サジェスト (FR-106)
- [ ] マッチ度計算と内訳表示 (FR-107)
- [ ] `JobCard` コンポーネント（8章の仕様に準拠）

### 1-2. 求人詳細
- [ ] SC-003 求人詳細。**労働条件の全項目を構造化テーブルで表示** (FR-201)
- [ ] モバイル固定応募CTA (FR-202)
- [ ] 類似求人 最大6件 (FR-209)
- [ ] 掲載終了求人の表示 (FR-210)
- [ ] SC-004 企業ページ
- [ ] SC-005 職種別 / SC-006 エリア別一覧

### 1-3. プロフィール
- [ ] SC-021 プロフィール編集（6セクション、セクション単位保存）(FR-301)
- [ ] 完成度％の算出と未入力セクションへの導線 (FR-302)
- [ ] SC-022 職務経歴（D&D並び替え、在職中フラグで終了年月を無効化）(FR-303)
- [ ] スキルタグ入力（マスタサジェスト + フリー入力）(FR-304)
- [ ] 2秒後の自動保存と保存状態表示 (FR-307)
- [ ] SC-029 設定：スカウト受付 ON/OFF (FR-305)、企業ブロック (FR-306)

### 1-4. 応募
- [ ] SC-010 応募フォーム3ステップ。入力内容を保持 (FR-204)
- [ ] 離脱確認ダイアログ (FR-205)
- [ ] `repo/applications.submit()`：重複チェック → `profileSnapshot` 凍結 → 通知配信 (FR-206, FR-207, FR-515)
- [ ] ゲストロールからの応募時のロール切替誘導と復帰 (FR-203)
- [ ] SC-023 応募履歴（ステータス別タブ、選考ステップ、辞退）(FR-208)
- [ ] SC-020 マイページ
- [ ] SC-027 お気に入り・保存検索 (FR-108)
- [ ] SC-028 通知センター (FR-701, FR-702)

### 1-5. トップ
- [ ] SC-001 トップページ

### Phase 1 完了チェック
- [ ] **フローAが完走する**（検索 → 詳細 → 応募 → 応募履歴に反映）
- [ ] リロードしても応募が残っている
- [ ] どの画面にも空の状態がない

---

## Phase 2 — 企業体験（4日）

### 2-1. 求人管理
- [ ] SC-111 求人作成ウィザード6ステップ、30秒ごと自動下書き保存 (FR-501)
- [ ] **必須項目バリデーション。未入力では掲載申請不可。不足項目をステップ単位で提示** (FR-502)
- [ ] 固定残業代 ON で内訳・時間数を必須化 (FR-503)
- [ ] 有期雇用選択時に契約期間・更新上限を必須化 (FR-504)
- [ ] 求職者視点のプレビュー (FR-505)
- [ ] SC-110 求人一覧（ステータス別タブ、複製）(FR-506)
- [ ] 掲載期限アラート (FR-507)
- [ ] SC-112 求人編集。公開中の編集は再審査対象であることを明示

### 2-2. 応募者管理（ATS）
- [ ] SC-120 応募者カンバン6列 (FR-508)
- [ ] dnd-kit による D&D ステータス変更 (FR-509)
- [ ] **キーボードでのステータス変更代替** (FR-510, A11Y-02)
- [ ] 「見送り」移動時の不採用通知選択 + テンプレート編集 + メールプレビュー (FR-511, FR-704)
- [ ] SC-121 応募者詳細（履歴書ビュー、5段階評価、社内メモ）(FR-513)
- [ ] 担当者アサインと絞り込み (FR-512)

### 2-3. ダッシュボード・企業ページ
- [ ] SC-100 企業ダッシュボード
- [ ] SC-101 企業ページ編集（画像アップロード、リッチテキストはサニタイズ）(NFR-10)
- [ ] SC-102 メンバー管理
- [ ] SC-160 プラン画面

### Phase 2 完了チェック
- [ ] **フローCの前半（求人作成 → 掲載申請）が完走する**
- [ ] 必須項目が1つでも欠けていると掲載申請できない
- [ ] **求職者ロールで応募 → 企業ロールに切替 → カンバンにそのカードがある**
- [ ] カンバンがキーボードのみで操作できる

---

## Phase 3 — スカウトとメッセージ（3日）

### 3-1. 候補者検索・スカウト
- [ ] SC-130 候補者検索。**`AnonymousCandidate` のみを扱う** (FR-401)
- [ ] **絞り込み条件に性別・年齢・国籍を含めない** (FR-402)
- [ ] SC-131 候補者詳細（匿名）
- [ ] SC-132 スカウト作成。テンプレート + 差し込み変数の置換 (FR-403)
- [ ] `repo/scouts.send()`：残枠検証 (FR-404)、30日再送制限 (FR-405)
- [ ] SC-024 スカウト一覧（求職者側）。「興味あり」でスレッド生成 + 氏名開示 (FR-406)
- [ ] SC-133 スカウト管理（開封率・返信率、テンプレート管理）

### 3-2. メッセージ
- [ ] SC-025 / SC-026 求職者側メッセージ
- [ ] SC-140 企業側メッセージ（2ペイン）
- [ ] チャットバブル4種（自分／相手／内部メモ／システム）(FR-410)
- [ ] 既読表示と求職者側の OFF 設定 (FR-408)
- [ ] ファイル添付（Object URL、10MB×3件まで）(FR-409)
- [ ] 連絡先らしき文字列の検出と注意表示 (FR-413)
- [ ] ロール切替でメッセージが双方向に反映されることを確認 (FR-407)

### 3-3. 日程調整
- [ ] 日程調整カード。最大5候補提示 → 1つ選択 (FR-411)
- [ ] 確定時に双方の通知センターへ配信 + ics ダウンロード (FR-412)
- [ ] 確定でカンバンのステータスを自動遷移

### Phase 3 完了チェック
- [ ] **フローBが完走する**（候補者検索 → スカウト → 興味あり → メッセージ → 日程確定）
- [ ] 「興味あり」より前に、企業側のどの画面からも候補者の氏名が見えないことを確認する

---

## Phase 4 — 運営・分析（2日）

- [ ] NGワード検出エンジン（年齢制限・性別限定・必須項目欠落・曖昧な給与表記）(FR-601)
- [ ] SC-201 求人審査（検出結果ハイライト、モーダル完結、次へ自動送り）(FR-603)
- [ ] 差戻し理由の定型選択 + 自由記述 → 企業の通知へ (FR-602)
- [ ] 承認 → 求職者側の検索結果に即時反映 (FR-604)
- [ ] SC-202 通報管理 (FR-605)
- [ ] SC-203 マスタ管理
- [ ] SC-200 運営ダッシュボード（Recharts で推移グラフ）(FR-606)
- [ ] SC-150 企業向け分析ダッシュボード（ファネル、期間指定）(FR-514)
- [ ] 通知種別ごとの ON/OFF (FR-703)

### Phase 4 完了チェック
- [ ] **フローCが完走する**（求人作成 → 審査 → 承認 → 検索結果に出現）

---

## Phase 5 — 仕上げ（3日）

### 5-1. 体験の総点検
- [ ] 4章「ロールを跨ぐ体験」の4パターンをすべて手動で確認する
- [ ] 全34画面を開き、**空の画面が1つもないこと**を確認する
- [ ] 全画面の空状態・ローディング・エラー状態を確認する
- [ ] ガイドツアーを実装 (FR-005)

### 5-2. アクセシビリティ監査
- [ ] axe-core を全画面に実行し、Critical / Serious を0件に
- [ ] キーボードのみで全画面を操作完遂する手動確認
- [ ] フォントサイズ200%で横スクロールが出ないことを確認 (A11Y-10)

### 5-3. パフォーマンス・SEO
- [ ] 画像を WebP/AVIF + 遅延読み込み (NFR-06)
- [ ] 初期JSバンドルを200KB以下に削減 (NFR-04)
- [ ] 全ページに固有の title / description / OGP画像 (NFR-09)
- [ ] Lighthouse で Performance 90 / Accessibility 95 / SEO 95 を達成 (NFR-07)

### 5-4. テストと公開
- [ ] Playwright で フローA・B・C の E2E を実装・実行
- [ ] Vitest：検索・マッチ度・バリデーション・リポジトリのカバレッジ80%以上
- [ ] `/dev/components` を本番で非公開に
- [ ] Vercel へデプロイ
- [ ] LP への導線設置
- [ ] 各フェーズのキャプチャを整理し、LP掲載用の時系列コンテンツにまとめる

---

**合計 16営業日（約3週間）**／1名専任 + レビュー体制。
各フェーズ完了時点でプレビューURLと画面キャプチャを残し、「Day 3 でここまで」「Day 7 でここまで」という時系列コンテンツとして LP に掲載する。爆速であることを、主張ではなく記録で示す。

---

# 12. 受入基準

1. 6章の優先度 P1 の要件がすべて実装されていること
2. 5章の主要フローA・B・Cが、エンドツーエンドで完走すること
3. 4章「ロールを跨ぐ体験」の4パターンがすべて動作すること
4. 「興味あり」より前に、企業側から候補者の氏名・連絡先が**どの経路からも見えない**こと
5. 労働条件の必須項目が未入力の求人は、掲載申請できないこと
6. 全34画面のいずれにも空の状態がなく、リアルなデータが表示されていること
7. リロード後も操作結果が保持され、「デモをリセット」で初期状態に戻ること
8. axe-core で Critical / Serious の指摘が0件であること
9. Lighthouse の基準値を全公開画面で満たすこと
10. データアクセスがすべて `lib/repo/` を経由しており、**コンポーネントが Zustand ストアを直接参照していない**こと

---

# 判断に迷ったときのルール

1. **仕様がこの文書にない場合は、実装せずに質問する。** 推測で作らない
2. **アクセシビリティと「空の画面を作らない」は削減対象外。** 速度を出すために削らない
3. スコープ外（3章 Won't have）は、思いついても実装しない
4. 「デモだから」を理由に品質を落とす判断はしない
5. コンポーネントからストアを直接触りたくなったら、それはリポジトリ層に足りないメソッドがあるということ。リポジトリに追加する
