# デリバリー・配送アプリ テンプレート 要件定義書
## プロダクト名：Souzoh Delivery

> シリーズ6本目。**シリーズ中で最も「状態」を持つ1本**です（カート・注文・配達進行が同時に動く）。
> 4条件（デザイン／画像自動取得／動き／第一印象）は本書でも必達要件として引き継ぎます。

---

## 1. 目的とゴール

| 項目 | 内容 |
|---|---|
| コンセプト | 近隣の店から料理・商品を注文し、**配達が進んでいく様子がリアルタイムに見える**デリバリーアプリ |
| 一次目的 | 「注文して、追跡する」という一連の流れをデモで通し切る |
| 二次目的 | 配達員視点（§9 D8）も用意し、**配送業・多店舗事業者**の読者に自社の配車/配送管理を想起させる |
| 成功条件 | 読者が1品カートに入れて注文し、**配達員が地図上を動くところ**まで到達する |
| 非ゴール | 実決済・実配車・実地図・店舗管理画面 |

**この題材の難所**：前5本は「見る」が中心でしたが、本作は**カート（複数商品＋オプション＋数量）**と**配達の時間進行**という2つの状態を同時に持ちます。§13でセッションを5分割し、カートを独立させているのはこのためです。

---

## 2. 引き継ぐ4条件

| 条件 | 対応章 | 本作での実現方法 |
|---|---|---|
| ① デザインにこだわる | §5 | **交通サイネージ路線**。極太コンデンス書体＋横帯構成＋蛍光オレンジ1色 |
| ② ネットから画像を自動取得 | §6 | Portalの `fetch-images.mjs` を流用。**本作は62枚**（料理写真が主役） |
| ③ 動きをつける | §7 | Portalの `motion.js` を継承 ＋ **本作固有の8種**。うち3種が配達追跡 |
| ④ 一目ですごい | §8 | **常駐ライブバンド**と**ルートリボン**の2点に集中投下 |

---

## 3. スコープ

### 3.1 作るもの

| # | 画面 | 優先度 | 既存資産との関係 |
|---|---|---|---|
| D1 | 店舗一覧（ホーム） | A | 母艦S1／Market K1を改修 |
| D2 | 店舗詳細（メニュー） | A | Market K2を改修 |
| D3 | 商品カスタマイズ | **S** | **新規**。オプション・数量・小計 |
| D4 | カート | **S** | **新規**。本作最大の状態管理 |
| D5 | 注文確定（配達先・時間・支払い） | A | Market の購入モーダルを拡張 |
| D6 | **配達追跡** | **S** | **新規**。本作の本体 |
| D7 | 注文履歴 | B | Market K6を改修 |
| D8 | 配達員モード | B | **新規**。1画面のみ（§9） |
| D9 | 検索・絞り込み | B | Market K3／Reviews R4を流用 |

### 3.2 作らないもの

- ❌ バックエンド・DB・認証・データ永続化
- ❌ **実地図（タイル配信）**。§3.3で代替案を規定
- ❌ 実決済・クーポン計算・ポイント
- ❌ 店舗側の受注管理画面
- ❌ 配達員の実位置取得（GPS）
- ❌ ダークモード、多言語、テストコード

### 3.3 地図の扱い ★重要な設計判断

Reviewsで地図を外した理由（外部依存・商用条件・見せ場にならない）は本作でも有効ですが、**デリバリーだけは「配達位置が見える」ことが体験の核**なので、単純に外すわけにいきません。

そこで**手描きSVGの抽象マップ**を採用します。

```
実地図タイル                        抽象SVGマップ（採用）
─────────────────────────────────────────────────
外部タイル配信に依存               依存ゼロ。ファイル1枚
商用利用条件が提供者ごとに違う     判断が発生しない
スクショが灰色で地味               配色を自分で決められる
ルート描画はライブラリ経由         SVGパスなので完全に制御できる
実装：ライブラリ＋API調整          実装：静的SVG＋path animation
```

**内容**：街区・幹線道路・川・公園を、`--color-line` の細線と `--color-surface-2` の面だけで抽象化した1枚のSVG（約900×600）。実在の地名は入れず、`さくら通り` `中央公園` のような架空表記にします。その上に注文の配達ルートを `stroke-dasharray` で描き、配達員のドットが `offset-path` で移動します。

> **これは妥協ではなく上位互換です。** グレーの地図タイルより、自分で配色した抽象マップのほうが記事のスクショとして明確に強い。実運用で本物の地図に差し替えたい場合の差し替え点は `features/tracking/RouteMap.jsx` の1ファイルだけになるよう、コンポーネント境界を切ってください。

---

## 4. 技術構成

```
Vite + React 18（JSX）
Tailwind CSS v4
lucide-react
motion（motion.dev）… §7.4で使用範囲を限定
────────── 既存資産から持ち込む ──────────
scripts/fetch-images.mjs        （Portal）
src/components/ui/SmartImage.jsx（Portal）
src/motion.js                   （Portal）
src/components/ui/Price.jsx     （Market）★本作で本領発揮
src/components/ui/Carousel.jsx  （Market）
src/components/ui/Stepper.jsx   （Market）★横向きに拡張
────────── 追加ライブラリなし ──────────
```

**`Price.jsx` がここで効きます。** 本作は商品価格・オプション加算・小計・配送料・手数料・合計と、金額の表示箇所が6種類あります。Marketで作った統一コンポーネントがなければ、ここで表記ゆれが必ず出ていました。

---

## 5. デザイン仕様 ★条件①

### 5.1 方向性

シリーズ6本の住み分けです。

| アプリ | 基調 | 書体 | 質感 |
|---|---|---|---|
| SNS | 明るいウォームグレー | サンセリフ | 余白 |
| Match | ディーププラム | サンセリフ | 舞台 |
| Market | 白 | サンセリフ | 陳列 |
| Portal | ディープインク | サンセリフ | ガラス／管制室 |
| Reviews | クリーム | セリフ | 雑誌 |
| **Delivery** | **ニュートラルグレー** | **極太コンデンス** | **交通サイネージ／物流** |

狙うのは**空港の発着案内板や配送伝票**の質感です。デリバリーの本質は「時間」と「現在地」なので、駅や空港の案内サインが持つ**即読性**を設計言語にします。

具体的には——

- **横帯（バンド）構成**。カードを並べるのではなく、画面を横方向の帯で切る
- **極太コンデンス書体**の大きな数字（`18分` `¥2,480`）
- 蛍光オレンジは**進行中の状態にのみ**使う。それ以外は無彩色
- 角丸は極小（6px）。サインボードの硬さ

### 5.2 シグネチャー①：常駐ライブバンド

**注文が進行中の間、画面下部（タブバーの直上）に横帯が常駐します。**

```
┌────────────────────────────────────────────┐
│ ● 調理中                    あと 18分  ›   │  ← 蛍光オレンジの帯
└────────────────────────────────────────────┘
```

- どの画面にいても消えない。タップでD6配達追跡へ（ボトムシートで迫り上がる）
- 左の `●` は状態色で明滅（#11パルス）
- 残り時間は**分単位で実際に減っていく**（デモでは加速。§7.3）
- 状態が変わるたびに帯のテキストが**上方向にスライドで入れ替わる**（#G）

> **これが本作で最も"アプリらしい"要素です。** 実装は固定配置の1コンポーネントですが、画面を移動しても注文が生き続けている感覚が出る。スクショにも必ず写るので、どのカットを撮っても「動いているアプリ」に見えます。

### 5.3 シグネチャー②：ルートリボン

D6の抽象マップ上を配達員のドットが移動します（§3.3・§7.2の#I）。

- ルートは `--color-accent` の3px実線。**通過済みは実線、未通過は破線**
- 配達員ドットは12pxの円＋外側に薄いパルス。`offset-path: path(...)` で移動
- 店舗ピンと配達先ピンは異なる形状（■と▼）で、色に頼らず区別
- 到着が近づくと（残り3分）、配達先ピンが1.6秒周期で拡大縮小

### 5.4 デザイントークン（`src/theme.css`）

```css
@theme {
  /* ---- Base ---- */
  --color-bg:        #F1F1EF;  /* ニュートラルグレー。紙でも白でもない */
  --color-surface:   #FFFFFF;
  --color-surface-2: #E5E5E2;  /* 帯・入力欄・マップの面 */
  --color-ink:       #121212;  /* 純黒に近い。サイネージの黒 */
  --color-ink-sub:   #6A6A66;
  --color-ink-inv:   #FFFFFF;
  --color-line:      #D6D6D2;

  /* ---- Accent（進行中にのみ使う） ---- */
  --color-accent:    #FF5A00;  /* 蛍光オレンジ */
  --color-accent-2:  #0B57D0;  /* 青：完了・確定済み */
  --color-ok:        #1B7F3B;
  --color-warn:      #B54708;  /* 遅延 */

  /* ---- 状態色（配達ステップ） ---- */
  --color-step-received: #6A6A66;  /* 注文受付 */
  --color-step-cooking:  #FF5A00;  /* 調理中 */
  --color-step-pickup:   #FF5A00;  /* 受け取り */
  --color-step-moving:   #FF5A00;  /* 配達中 */
  --color-step-done:     #1B7F3B;  /* 到着 */

  /* ---- Type ---- */
  --font-display: "Archivo Expanded", "Anton", "Zen Kaku Gothic New", sans-serif; /* 700/800 */
  --font-body:    "Noto Sans JP", sans-serif;                                     /* 400/500 */
  --font-num:     "Roboto Condensed", sans-serif;  /* ★極太コンデンス。時間・金額 */

  --text-eta:    clamp(44px, 6vw, 76px)/0.95;  /* 「18分」の巨大表示 */
  --text-hero:   clamp(26px, 3.2vw, 38px)/1.15;
  --text-title:  18px/1.4;
  --text-body:   14px/1.7;
  --text-label:  11px/1.2;   /* 大文字＋letter-spacing .14em */

  /* ---- Space ---- */
  --space-1: 4px;  --space-2: 8px;  --space-3: 12px;
  --space-4: 16px; --space-6: 24px; --space-8: 32px;

  /* ---- Shape ---- */
  --radius-card:  6px;    /* シリーズ最小。サインボードの硬さ */
  --radius-input: 6px;
  --radius-pill:  999px;
  --shadow-card:  none;
  --shadow-band:  0 -8px 24px rgba(18,18,18,.10);  /* 常駐バンドのみ */
  --shadow-pop:   0 16px 48px rgba(18,18,18,.20);

  /* ---- Motion（Portalから継承） ---- */
  --ease-out:   cubic-bezier(.16,1,.3,1);
  --ease-inout: cubic-bezier(.65,0,.35,1);
  --dur-fast:   140ms;
  --dur-base:   240ms;
  --dur-slow:   520ms;
}
```

### 5.5 タイポグラフィの決め事

- **時間と金額は必ず `--font-num`（極太コンデンス）＋ `tabular-nums`**。ETAの `18分` が巨大なコンデンス数字で出るだけで、このアプリの顔になります
- ラベル（`配達先` `お届け時間` など）は `--text-label` ＋ `letter-spacing: .14em` ＋ `--color-ink-sub`
- 見出しは `--font-display`（エクスパンデッド／コンデンスの太字）。**本文には使わない**
- 状態名（`調理中` `配達中`）は必ず大きめの太字。小さい灰色文字にしないこと

### 5.6 やらないこと（NGリスト）

- ❌ オレンジを装飾に使う（**進行中の状態にのみ**。ボタンの色ですら、進行に関係なければ黒）
- ❌ 料理写真に強い角丸（`--radius-card` 6pxを超えない）
- ❌ カードへの影（常駐バンドのみ `--shadow-band`）
- ❌ 状態を色だけで区別する（必ずアイコン形状かラベルを併記。§5.3のピン形状もこの原則）
- ❌ 角丸の混在（3種のみ）
- ❌ 「🍕」等の絵文字をカテゴリアイコンに使う（lucide-reactに統一）

---

## 6. 画像の自動取得 ★条件②

### 6.1 方針

Portalの `scripts/fetch-images.mjs` を**無変更でコピー**。`imageManifest.js` のみ差し替えます。表示側も `SmartImage.jsx` の3層フォールバックをそのまま使用。仕様はPortal §6.3と同一のため本書では繰り返しません。

### 6.2 枚数：62枚

**シリーズ最多です。** デリバリーは料理写真がそのまま商品価値なので、ここは削れません。

| 用途 | 枚数 |
|---|---|
| 店舗カバー | 10 |
| メニュー商品写真 | 40（1店舗あたり4品） |
| カテゴリバナー | 6 |
| 特集・キャンペーン | 3 |
| 配達員アバター | 0（**§6.3の通り生成**） |
| 空状態・完了画面のイラスト代替 | 3 |
| **合計** | **62** |

並列4本で40秒程度です。**Session 0で必ず取得を完了させてください**（§13）。

### 6.3 配達員・ユーザーのアバター

Portal／Reviewsと同じ判断で、**実在人物の写真は使いません**。ID由来グラデーション＋頭文字です。

本作では配達員の顔が追跡画面に出るため特に重要です。架空の配達員に実在の人物の顔を充てた画面を、テンプレートとして配布すべきではありません。

### 6.4 マニフェスト抜粋

```js
export default [
  // 店舗カバー（10）
  { seed:"st-kamameshi", w:1200, h:800, use:"釜飯 とよ" },
  { seed:"st-thai-lek",  w:1200, h:800, use:"タイ食堂 レック" },
  { seed:"st-burger-9",  w:1200, h:800, use:"NINE BURGER" },
  { seed:"st-soba-koke", w:1200, h:800, use:"手打ちそば 苔" },
  { seed:"st-curry-tomo",w:1200, h:800, use:"カリー トモ" },
  { seed:"st-deli-mori", w:1200, h:800, use:"デリカ もり" },
  { seed:"st-pizza-fumo",w:1200, h:800, use:"PIZZA FUMO" },
  { seed:"st-cafe-yuru", w:1200, h:800, use:"ゆるり珈琲" },
  { seed:"st-sweets-ao", w:1200, h:800, use:"洋菓子 あお" },
  { seed:"st-mart-24",   w:1200, h:800, use:"まるとくマート" },

  // メニュー商品（40）… {店舗seed}-m1 〜 -m4、すべて 800×800（正方形）
  // カテゴリバナー（6）… cat-washoku / cat-asia / cat-burger / cat-cafe / cat-sweets / cat-mart（900×400）
  // 特集（3）……… feature-fast / feature-solo / feature-late（1400×500）
  // 空状態（3）…… empty-cart / empty-search / done-thanks（800×600）
];
```

---

## 7. 動き ★条件③

### 7.1 Portalからの継承

| # | 名称 | 本作での適用先 |
|---|---|---|
| 1 | 段差入場 | D1の店舗カード（50ms差） |
| 2 | カウントアップ | 合計金額・ETA |
| 4 | スクロール連動リビール | D2のメニュー、D1下部 |
| 6 | View Transitions 画面遷移 | 全ページ |
| 7 | 共有要素モーフ | 店舗カード → D2、商品 → D3 |
| 9 | リスト入れ替え | D9絞り込み結果、D4カート内 |
| 11 | ライブパルス | 常駐バンドの `●`、配達員ドット |
| 14 | トースト | 全体 |

### 7.2 本作固有の動き（8種）

| # | 名称 | 対象 | 仕様 | reduced時 |
|---|---|---|---|---|
| G | **バンドのテキスト入れ替え** | 常駐バンド | 状態変化時、旧テキストが上へ抜け新テキストが下から入る。各220ms、`--ease-out` | 即差し替え |
| H | **カート投入の飛翔** | D3 → カート | 商品サムネがカートアイコンへ弧を描いて縮小移動（480ms）＋着地時にバッジが1.25倍→戻る | バッジ数のみ変化 |
| I | **配達員の移動** | D6ルートリボン | `offset-path` に沿ってドットが進行。ルートの実線部分が同期して伸びる | 現在地に静止表示 |
| J | **ステップ帯の進行** | D6上部 | 横向きStepper。次ステップへ移る際、帯が左から右へ塗り替わる（600ms） | 即塗り替え |
| K | **ETAカウントダウン** | D6中央 | 巨大コンデンス数字。分が変わる瞬間だけ上方向スライド（180ms） | 数字のみ変化 |
| L | **オプション選択の小計反映** | D3 | オプションを選ぶたびに小計が180msでカウント変化 | 即変化 |
| M | **カート行のスワイプ削除** | D4 | 左スワイプで削除ボタン露出（判定64px）。削除時は高さ0へ縮小（280ms） | ボタン常時表示 |
| N | **到着セレブレーション** | D6完了時 | 配達先ピンから同心円が2回広がる（900ms）＋「お届け完了」が拡大表示 | テキストのみ |

### 7.3 デモの時間圧縮 ★重要

実際の配達は30分かかりますが、デモは**90秒で全ステップを通します**。

| 経過 | 状態 | ETA表示 |
|---|---|---|
| 0秒 | 注文受付 | 32分 |
| 8秒 | 調理中 | 26分 |
| 30秒 | 店舗で受け取り | 18分 |
| 45秒 | 配達中（ドット移動開始） | 12分 |
| 80秒 | まもなく到着（配達先ピン拡大） | 3分 |
| 90秒 | お届け完了（#N） | — |

- ETAは**リアルタイムで減り続ける**（表示は分単位、内部は秒管理）
- D6を開いていなくても進行する（常駐バンドが更新される）
- 進行速度は `src/config/demo.js` の1定数（`DEMO_SPEED = 20`）で制御。**実時間にしたい場合は1にするだけ**

> ★90秒という設定が要点です。記事の読者が「注文 → 到着」を1回通せる長さで、かつ待たされている感じがしない。長すぎると誰も最後まで見ません。

### 7.4 motion ライブラリの使用範囲（限定）

- ✅ #7 共有要素モーフ
- ✅ H カート投入の飛翔
- ✅ M スワイプ削除の高さアニメーション
- ✅ #9 リスト入れ替え
- ❌ 上記以外はCSS / WAAPI / View Transitions（**I・J・K・N はすべて自前**。`offset-path` と `stroke-dashoffset` で足ります）

---

## 8. 「一目ですごい」の設計 ★条件④

### 8.1 勝負するのはD6（配達追跡）

Portalはホーム、Reviewsは店舗詳細、**本作は配達追跡**です。ここが記事のサムネイルになります。

D6のファーストビューに以下が同時に載ります。

1. **巨大なコンデンス数字のETA**（`18`分）が減り続けている
2. **抽象マップ上を配達員のドットが動いている**
3. ルートの実線が**伸びている**
4. 上部の**ステップ帯**が塗り替わる
5. 配達員名・車両種別・評価の小さな行
6. 下部に注文内容のサマリー

> **「動いているスクリーンショット」を撮るのは不可能ですが、動いているアプリの静止画は伝わります。** ルートの実線／破線の切り替わり位置に配達員ドットがあり、ETAが半端な数字（`18分`）である。この2点だけで「今まさに動いている」ことが静止画から読み取れます。

### 8.2 D6のレイアウト（モバイル基準・PCは中央600px）

```
┌────────────────────────────────────────┐
│ ①受付 ─ ②調理 ─ ③受取 ─ ④配達 ─ ⑤到着 │ ← 横ステップ帯（J）
├────────────────────────────────────────┤
│                                        │
│        抽象マップ（SVG）                │
│         ■店 ━━━━●┈┈┈┈▼配達先          │ ← ルートリボン（I）
│                                        │
├────────────────────────────────────────┤
│  あと                                  │
│  18分            ← --text-eta（K）      │
│  12:40 - 12:50 に到着予定               │
├────────────────────────────────────────┤
│ ▨ 佐々木さんが配達中    自転車 / ★4.9  │
├────────────────────────────────────────┤
│ 釜飯 とよ ・ 3品 ・ ¥2,480             │
│ 注文内容を見る ›                        │
└────────────────────────────────────────┘
```

PCでは左にマップ、右にETA＋配達員＋注文内容の2カラム。

### 8.3 常駐ライブバンドの効き

§5.2の通り。**どの画面のスクショにも必ず写る**ため、記事に載せる画像が全部「注文が進行中のアプリ」になります。1コンポーネントの実装で、全カットの印象が変わる投資です。

---

## 9. 画面仕様

### D1. 店舗一覧（ホーム）

- 上部：配達先の表示（`蔵前2丁目 ›`。タップでトースト「デモでは変更できません」）＋ 検索
- カテゴリバナー横スクロール（6枚）
- フィルタチップ：`配達が速い` `送料無料` `新着` `評価4.5以上`
- 特集バナー3枚
- 店舗カード（横帯レイアウト）：
  - 左に正方形のカバー写真（96px）、右に店名・ジャンル・`18-28分`・`送料 ¥150`・`★4.6(212)`
  - **配達時間を `--font-num` で強調**。デリバリーで最も見られる数字はここ
  - 営業時間外は写真を `grayscale(1) opacity(.5)` ＋ `準備中` 帯

### D2. 店舗詳細（メニュー）

- カバー写真（高さ200px）＋ 下端グラデーション
- 店名・ジャンル・`18-28分`・`送料 ¥150`・`最低注文 ¥800`・`★4.6`
- カテゴリのスティッキータブ（`人気` `ごはん` `サイド` `ドリンク`）。スクロール連動で現在地がハイライト
- メニュー行：左に商品名・説明1行・価格、右に正方形写真（80px）
- 品切れは `--color-ink-sub` に脱色 ＋ `品切れ` バッジ、タップ不可
- 下部：カートに商品があれば `カートを見る（3品・¥2,480）` の黒帯

### D3. 商品カスタマイズ ★固有画面

ボトムシート（PCは中央モーダル）。

| # | 要素 | 仕様 |
|---|---|---|
| 1 | 商品写真 | 1:1、上部に大きく |
| 2 | 商品名・説明・基本価格 | |
| 3 | **必須オプション** | ラジオ。例：`サイズ`（並 +¥0 / 大 +¥200 / 特盛 +¥380）。未選択では追加不可 |
| 4 | **任意オプション** | チェックボックス。例：`トッピング`（温玉 +¥80 / のり +¥50 / ねぎ増し +¥30）。**最大3つまで** |
| 5 | 要望メモ | 100字。プレースホルダ「アレルギーやご要望があればご記入ください」 |
| 6 | 数量 | `−` `1` `+`。1〜20 |
| 7 | 追加ボタン | `カートに追加  ¥1,180`（**選択に応じて金額がリアルタイム変化**＝動きL） |

追加 → シート下降 ＋ **カート飛翔アニメーション（H）** ＋ トースト。

### D4. カート ★固有画面・状態管理の本体

- 店舗名（**1店舗のみ**。別店舗の商品を追加しようとすると確認ダイアログ「カートを空にして新しく注文しますか？」）
- 商品行：写真・商品名・選択したオプション（`大 / 温玉 / のり`）・数量ステッパー・小計
- **左スワイプで削除**（動きM）。削除時は `元に戻す` 付きトースト（5秒）
- `＋ 商品を追加` → D2へ戻る
- 金額内訳（`Price.jsx` を使用）：

```
商品小計                  ¥ 2,180
配送料                    ¥   150
サービス料 5%             ¥   109
────────────────────────────────
合計                      ¥ 2,439   ← --font-num / 大きく
```

- 最低注文金額未達の場合：`あと ¥320 で注文できます` を表示し、確定ボタンを非活性
- 空状態：`empty-cart` 画像 ＋ `お店をさがす`

### D5. 注文確定

1ページに縦積み（モーダルにしない。項目が多いため）。

| # | 項目 | 仕様 |
|---|---|---|
| 1 | 配達先 | ダミー住所プリセット ＋ `建物名・部屋番号` の入力欄（任意） |
| 2 | 受け取り方法 | 2択：`手渡し` / `置き配`。置き配を選ぶと `置き場所` の3択が出現 |
| 3 | 配達時間 | 2択：`できるだけ早く（18-28分）` / `時間を指定` → 30分刻みのセレクト |
| 4 | 支払い方法 | 3択ラジオ（カード／QR決済／現金） |
| 5 | 配達員へのメモ | 100字 |
| 6 | 金額内訳 | D4と同一表示 |

`注文を確定する` → 確定アニメーション → **D6へ自動遷移 ＋ 常駐バンド起動 ＋ §7.3のタイマー開始**

### D6. 配達追跡 ★本体

§8.2の構成。加えて：

- ステップ帯の各ノードをタップすると、その工程の説明がポップオーバーで出る
- `配達員にメッセージ` → 母艦S7のチャットを流用（送信1.2秒後に定型返信）
- 到着後（#N）：`配達を評価する`（星3択 ＋ コメント任意）→ 送信でD7へ
- **注文が無い状態でD6を開いた場合**：直近の完了注文を表示 ＋ `もう一度注文する`

### D7. 注文履歴

- タブ：`進行中` / `完了`
- 行：店舗写真・店名・注文日時・品数・合計・状態バッジ
- `もう一度注文する` → 同じ内容でカートに投入（**オプションも復元**）
- タップで注文詳細（明細＋配達実績の時刻）

### D8. 配達員モード ★1画面のみ

**設けた理由**：読者に配送業・多店舗運営が多いためです。「顧客側だけでなく配達側の画面も作れる」ことを1画面で示せば、相談の入口が広がります。**あくまで1画面**に留め、コストを膨らませません。

- ヘッダーのアイコンから切替（`お客様 ⇄ 配達員`）
- 内容：
  - オンライン／オフラインのトグル（オンライン時のみ以下を表示）
  - **新着依頼カード**：店舗名・受取先・配達先・距離 `2.1km`・報酬 `¥620`・`受ける` / `見送る`
  - 受注すると、進行中タスクとして `店舗へ向かう → 受け取り完了 → 配達先へ → 配達完了` の4ボタン順送り
  - 本日の実績：`完了 8件` `走行 21.4km` `報酬 ¥5,240`（すべて `--font-num`）
- **顧客側D6の状態と連動させません**（別データ。連動させると状態管理が跳ね上がります）

### D9. 検索・絞り込み

Market K3／Reviews R4と同構造。`ジャンル` `配達時間（20分以内/30分以内）` `送料` `評価` `営業中のみ`。

---

## 10. データモデル

```js
// store
{ id:"st01", name:"釜飯 とよ", genre:"和食",
  cover:{ seed:"st-kamameshi", w:1200, h:800 },
  etaMin:18, etaMax:28, deliveryFee:150, minOrder:800,
  score:4.6, reviewCount:212,
  hours:{ open:"11:00", close:"21:30" }, open:true,
  categories:["人気","ごはん","サイド","ドリンク"] }

// menuItem
{ id:"mi01", storeId:"st01", category:"人気",
  name:"五目釜飯", desc:"季節の具材を土鍋で炊き上げます",
  price:980, image:{ seed:"st-kamameshi-m1" }, soldOut:false,
  optionGroups:[
    { id:"g1", label:"サイズ", required:true, max:1,
      options:[{id:"o1",label:"並",add:0},{id:"o2",label:"大",add:200},{id:"o3",label:"特盛",add:380}] },
    { id:"g2", label:"トッピング", required:false, max:3,
      options:[{id:"o4",label:"温玉",add:80},{id:"o5",label:"のり",add:50},{id:"o6",label:"ねぎ増し",add:30}] }
  ] }

// cartLine
{ lineId:"cl01", menuItemId:"mi01", qty:2,
  selected:{ g1:"o2", g2:["o4","o5"] }, note:"",
  unitPrice:1310 }          // 基本980 + 大200 + 温玉80 + のり50

// order
{ id:"od01", storeId:"st01", lines:[...],
  subtotal:2180, deliveryFee:150, serviceFee:109, total:2439,
  method:"手渡し"|"置き配", placedAt:"...",
  step:1..5, etaSec:1920,           // 秒管理。表示は分
  courier:{ name:"佐々木", vehicle:"自転車", score:4.9, avatarSeed:"c01" },
  route:"M 80 460 L 240 460 L 240 300 L 420 300 L 420 150 L 660 150",  // SVGパス
  progress:0.0 }                     // 0→1。ドット位置とルート実線に使う

// 金額計算（定数は1箇所に）
subtotal   = Σ(unitPrice × qty)
serviceFee = Math.floor(subtotal * SERVICE_RATE)   // SERVICE_RATE = 0.05
total      = subtotal + deliveryFee + serviceFee
```

**`route` をSVGパス文字列で持つ**のが要点です。配達員ドットは `offset-path: path(var(--route))`、ルートの実線は同じパスの `stroke-dashoffset` を `progress` から計算。**1つのデータで2つの表現が同期します。**

---

## 11. デモデータ

### 11.1 店舗（10件）

| id | 店名 | ジャンル | ETA | 送料 | 最低注文 | 評価 | 件数 | 状態 |
|---|---|---|---|---|---|---|---|---|
| st01 | 釜飯 とよ | 和食 | 18-28分 | ¥150 | ¥800 | 4.6 | 212 | 営業中 |
| st02 | タイ食堂 レック | アジア | 22-32分 | ¥200 | ¥1,000 | 4.4 | 389 | 営業中 |
| st03 | NINE BURGER | バーガー | 15-25分 | ¥0 | ¥1,200 | 4.2 | 741 | 営業中 |
| st04 | 手打ちそば 苔 | 和食 | 25-35分 | ¥250 | ¥900 | 4.7 | 96 | 営業中 |
| st05 | カリー トモ | カレー | 20-30分 | ¥150 | ¥800 | 4.5 | 458 | 営業中 |
| st06 | デリカ もり | 惣菜 | 12-20分 | ¥100 | ¥600 | 4.1 | 167 | 営業中 |
| st07 | PIZZA FUMO | ピザ | 28-40分 | ¥0 | ¥1,800 | 4.3 | 523 | 営業中 |
| st08 | ゆるり珈琲 | カフェ | 15-22分 | ¥180 | ¥500 | 4.8 | 134 | 営業中 |
| st09 | 洋菓子 あお | スイーツ | 20-30分 | ¥200 | ¥700 | 4.6 | 88 | **準備中** |
| st10 | まるとくマート | 食料品 | 30-45分 | ¥300 | ¥1,500 | 3.9 | 1,024 | 営業中 |

> ★ETAと送料を意図的にばらけさせています。全店が同条件だとフィルタ（`配達が速い` `送料無料`）が機能しているように見えません。`st03` `st07` の送料¥0、`st06` の12-20分がフィルタの効きを示します。

### 11.2 メニュー（40品・st01を全量、他店は要約）

**釜飯 とよ（st01）**

| id | カテゴリ | 商品名 | 説明 | 価格 | 品切れ |
|---|---|---|---|---|---|
| mi01 | 人気 | 五目釜飯 | 季節の具材を土鍋で炊き上げます | 980 | — |
| mi02 | 人気 | 鶏ごぼう釜飯 | 香ばしいごぼうと柔らかい鶏 | 1,080 | — |
| mi03 | サイド | だし巻き玉子 | 出汁をたっぷり含ませています | 480 | — |
| mi04 | ドリンク | ほうじ茶（冷） | 自家焙煎 | 220 | ✓ |

オプション構成は§10の `mi01` を全商品の雛形にし、**カテゴリごとに内容を変える**：

| カテゴリ | 必須オプション | 任意オプション |
|---|---|---|
| ごはん類 | サイズ（並/大/特盛） | トッピング（最大3） |
| バーガー | セット（単品/ポテト/ドリンク付） | トッピング（最大3） |
| ドリンク | サイズ（S/M/L）・氷（あり/なし） | — |
| ピザ | サイズ（M/L）・生地（薄/厚） | トッピング（最大3） |
| スイーツ | — | メッセージプレート（+¥300） |

他9店舗は各4品（人気2・サイド1・ドリンク1）で構成。**全40品に写真seed（`{店舗seed}-m1`〜`-m4`）を割り当てます。**

### 11.3 進行中の注文（初期状態）

**初期は注文なし**にします。読者が自分でカートに入れて注文する体験を通してほしいためです。

ただし**D7注文履歴には完了済み3件**を用意し、`もう一度注文する` から1タップで注文できるようにします。**急いでいる読者はこちらから90秒シーケンスに入れます。**

| 店舗 | 注文日時 | 内容 | 合計 |
|---|---|---|---|
| st05 カリー トモ | 7/21 12:34 | チキンカレー（辛さ中辛・ライス大）× 1、ラッシー × 1 | ¥1,690 |
| st03 NINE BURGER | 7/18 19:02 | ダブルチーズ（ポテトセット）× 2 | ¥2,760 |
| st08 ゆるり珈琲 | 7/16 09:15 | ドリップ（M・氷なし）× 2、スコーン × 1 | ¥1,340 |

### 11.4 配達員（4名・ローテーション）

| id | 名前 | 車両 | 評価 |
|---|---|---|---|
| c01 | 佐々木 | 自転車 | 4.9 |
| c02 | 東 | バイク | 4.7 |
| c03 | ナディア | 自転車 | 5.0 |
| c04 | 三好 | 軽自動車 | 4.6 |

### 11.5 D8 配達員モードの依頼（3件）

| 店舗 | 受取 | 配達先 | 距離 | 報酬 |
|---|---|---|---|---|
| st01 釜飯 とよ | 蔵前1丁目 | 浅草橋3丁目 | 2.1km | ¥620 |
| st03 NINE BURGER | 蔵前3丁目 | 蔵前1丁目 | 0.9km | ¥480 |
| st10 まるとくマート | 両国2丁目 | 蔵前4丁目 | 3.4km | ¥880 |

本日の実績：`完了 8件` / `走行 21.4km` / `報酬 ¥5,240`

---

## 12. ディレクトリ構成

```
souzoh-delivery/
├── scripts/
│   └── fetch-images.mjs        ← Portalからコピー（無変更）
├── public/images/              ← .gitignore対象
├── src/
│   ├── theme.css               ★差し替え（§5.4）
│   ├── app.config.js           ★差し替え（下記）
│   ├── motion.js               ← Portalからコピー ＋ G〜Nを追記
│   ├── config/demo.js          ★新規：DEMO_SPEED / SERVICE_RATE 等の定数
│   ├── data/
│   │   ├── stores.js  menuItems.js  orders.js
│   │   ├── couriers.js  jobs.js  imageManifest.js
│   ├── components/ui/          ← 母艦＋Market＋Portalからコピー
│   │   ├── SmartImage.jsx      ← Portal（無変更）
│   │   ├── Price.jsx           ← Market（無変更）★本作で6箇所に使用
│   │   ├── Carousel.jsx        ← Market（無変更）
│   │   ├── StepBar.jsx         ★新規：Stepperの横向き版（動きJ）
│   │   ├── LiveBand.jsx        ★新規：常駐バンド（§5.2・動きG）
│   │   ├── QtyStepper.jsx      ★新規：− 1 ＋
│   │   └── SwipeRow.jsx        ★新規：スワイプ削除（動きM）
│   ├── components/shell/       ← 母艦からコピー（LiveBandの高さ分だけ下部余白を確保）
│   ├── features/
│   │   ├── stores/  store-detail/  search/  history/
│   │   ├── customize/          ★新規：D3
│   │   ├── cart/               ★新規：D4
│   │   ├── checkout/           ★新規：D5
│   │   ├── tracking/           ★新規：D6
│   │   │   ├── RouteMap.jsx    ★実地図に差し替える場合の唯一の変更点（§3.3）
│   │   │   └── map-base.svg    ★抽象マップの下地
│   │   ├── courier/            ★新規：D8
│   │   └── chat/               ← 母艦 dm/ をそのまま
│   ├── store.js                ★カートと注文進行を持つ。本作で最も厚い
│   └── main.jsx
└── README.md
```

```js
// app.config.js
export default {
  appName: "Souzoh Delivery",
  tagline: "いま、どこにいるかが見える",
  layout: { maxWidth: 600 },
  nav: [
    { key:"stores",  label:"お店",     icon:"Store" },
    { key:"search",  label:"さがす",   icon:"Search" },
    { key:"cart",    label:"カート",   icon:"ShoppingBag", badge:"cartCount" },
    { key:"history", label:"注文履歴", icon:"Receipt" },
    { key:"mypage",  label:"マイページ", icon:"User" },
  ],
  labels: {
    listTitle: "お届けできるお店",
    itemNoun: "商品",
    createCta: "カートに追加",
    primaryAction: "お気に入り",
  },
};
```

> ★`nav` に **`badge` プロパティ**を追加しています。カート個数を表示するためで、母艦の `shell/` に汎用のバッジ対応を入れてください（他アプリでは未使用のまま無害です）。

---

## 13. 実装手順（Claude Codeへの投げ方）

**5分割。** Reviewsより1つ多いのは、カート（D3・D4）が独立した重さを持つためです。

| Session | やること | 冒頭で読ませるもの |
|---|---|---|
| 0 | 母艦を複製 → Portal/Marketから `fetch-images.mjs` `SmartImage` `Price` `Carousel` `motion.js` をコピー → `theme.css` `app.config.js` `data/` `config/demo.js` を差し替え → **`npm run fetch:images` で62枚取得** → `shell/` にバッジ対応を追加 → ビルドを通す | §5.4・§6・§11・§12 |
| 1 | `ui/` の4点（StepBar / LiveBand / QtyStepper / SwipeRow）＋ `motion.js` にG〜N追記 ＋ **`store.js` のカート／注文の状態設計** | §5.2・§7.2・§10 |
| 2 | D1店舗一覧 ＋ D2店舗詳細 | §9(D1,D2) |
| 3 | **D3カスタマイズ ＋ D4カート ＋ D5注文確定**（金額計算をここで完成させる） | §9(D3〜D5)・§10 |
| 4 | **D6配達追跡**（抽象マップ・ルート・タイマー・§7.3のシーケンス） | §3.3・§7.3・§8.2・§9(D6) |
| 5 | D7履歴 ＋ D8配達員モード ＋ D9検索 ＋ 仕上げ | §9(D7〜D9)・§14 |

**Session 1で `store.js` の状態設計を先に固めてください。** 本作の難所はUIではなくカートの状態です。「同一商品でもオプションが違えば別行」「別店舗の商品追加時はカートを空にする」「削除の取り消し」——ここを画面と一緒に作ると、D4で必ず作り直しになります。**状態だけ先に設計し、コンソールで動作確認してから画面に入る**のが最短です。

**Session 4 の指示文テンプレ**：

```
添付の要件定義書 §3.3・§7.3・§8.2・§9(D6) を実装してください。
src/components/ui/ の StepBar / LiveBand / SmartImage と src/motion.js は完成済みです。
新規作成せず import して使ってください。
地図は外部ライブラリを使わず、静的SVG（map-base.svg）＋ order.route のパス文字列で
実装してください。配達員ドットは offset-path、ルートの実線は stroke-dashoffset を使い、
どちらも order.progress の単一の値から算出してください。
進行速度は src/config/demo.js の DEMO_SPEED のみを参照し、
JSX に秒数を直接書かないでください。
実装前に、作成・変更するファイルの一覧だけ先に提示してください。
```

---

## 14. 受け入れ基準

**条件①（デザイン）**
- [ ] ETAと金額が極太コンデンス（`--font-num`）＋ `tabular-nums`
- [ ] オレンジが**進行中の状態以外**に使われていない
- [ ] 状態が色だけで区別されていない（アイコン形状かラベルを併記）
- [ ] カードに影がない（常駐バンドのみ例外）
- [ ] 生の色コード・生のpx値がJSXに1つも無い
- [ ] §5.6のNGリストに1つも該当しない

**条件②（画像）**
- [ ] `npm run fetch:images` で62枚が取得される
- [ ] `public/images/` を空にしても壊れた画像アイコンが出ない
- [ ] 配達員アバターに実在人物の写真が使われていない
- [ ] `public/images/` が `.gitignore` に入っている

**条件③（動き）**
- [ ] §7.2のG〜N、§7.1の継承分がすべて動作する
- [ ] §7.3の90秒シーケンスが通り、**D6を開いていなくても進行する**
- [ ] `DEMO_SPEED` を1に変えると実時間になる
- [ ] `prefers-reduced-motion: reduce` で「停止」列がすべて止まる

**条件④（第一印象）**
- [ ] D6でETAが減り、配達員ドットが動き、ルートの実線が伸びる（3つが同期）
- [ ] 常駐バンドがどの画面でも表示され、状態変化でテキストが入れ替わる

**機能（本作の要）**
- [ ] 同一商品でもオプションが違えばカートで別行になる
- [ ] 別店舗の商品を追加しようとすると確認ダイアログが出る
- [ ] オプション選択で `カートに追加 ¥○○` の金額がリアルタイム変化する
- [ ] 必須オプション未選択では追加できない／任意オプションは最大3つで打ち止め
- [ ] 最低注文金額未達で `あと ¥○○` が表示され、確定できない
- [ ] スワイプ削除に `元に戻す` が効く
- [ ] `もう一度注文する` でオプションまで復元される
- [ ] 置き配を選ぶと置き場所の3択が出現する
- [ ] 合計 = 商品小計 + 配送料 + サービス料5% が全画面で一致する
- [ ] D8配達員モードが4ボタンで順送りできる

**共通**
- [ ] 390px / 1024px の2幅で崩れない
- [ ] `npm run build` が警告なく通り、Cloudflare Pagesで表示される

---

## 15. シリーズ資産の状況（6本目終了時点）

| 資産 | 出所 | 本作での扱い |
|---|---|---|
| `components/ui/` 8点 | 母艦SNS | そのまま |
| `components/shell/` | 母艦（+maxWidth +badge） | **本作でbadge対応を追加** |
| `Price.jsx` | Market | **6箇所で使用**。投資回収 |
| `Carousel.jsx` | Market | メニュー写真に流用 |
| `Stepper.jsx` | Market | → `StepBar.jsx`（横向き）に派生 |
| `SmartImage.jsx` | Portal | そのまま |
| `fetch-images.mjs` | Portal | そのまま |
| `motion.js` | Portal | コピー＋追記 |
| `Stars` 系 | Reviews | 店舗評価表示に流用 |
| `QtyStepper` / `SwipeRow` / `LiveBand` | **本作** | → 学習アプリのカート的UIに転用可 |

**新規実装は `ui/` 4点と画面5枚。** 6本目にしてもなおD3・D4・D6という重い3画面があるため、Reviewsほど軽くはなりません。ただし前5本が無ければ、この規模は成立していません。

---

## 16. 補足：コスト感

**Marketと並ぶ重量級**です。Marketが「状態遷移の複雑さ」、Portalが「演出の作り込み」だったのに対し、本作は**カートの状態設計**が重さの中心です。

費用が伸びる箇所は事前に特定できています。

1. **カートの状態設計**（オプション違いの同一商品、別店舗の扱い、削除の取り消し）
   → §13のSession 1で画面と切り離して先に作ることで潰します
2. **配達アニメーションの3要素同期**（ドット位置・ルート実線・ETA）
   → §10で `progress` という**単一の値から全部を導出**する設計にしてあります。3つを別々に管理すると必ずズレて往復が発生します
3. **時間進行のデバッグ**
   → `DEMO_SPEED` を1定数にしてあるので、開発中は100倍速で確認できます

逆に、条件②③がPortal資産の持ち込みで済むこと、金額表示がMarketの `Price.jsx` で統一済みであることが効いています。**シリーズを1本目から順に作ってきた効果が、最も金額として表れるのがこの1本**です。
