# Mobile BFF β-full 設計書

> **For Claude:** REQUIRED SUB-SKILL: Use `godmode:task-runner` (または `godmode:delegated-execution`) to implement this plan task-by-task.

**作成日**: 2026-04-23  
**決定経緯**: 2026-04-23 設計セッション — モバイルアプリ専用 BFF の方針を β-full（同一 Worker 上に `/v1/mobile/*` 名前空間を全面展開）に決定

---

## Goal

Flutter モバイルアプリが叩くすべての API を `/v1/mobile/*` 名前空間に集約し、外部モバイル開発委託先が `/v1/mobile/*` のみを参照すれば実装できる状態を作る。

- **Aggregate endpoint（5本）**: 複数サービス呼び出しをサーバー側でまとめ、画面 1 つ = API 1 本を実現
- **Thin wrapper（34本）**: 既存サービス関数をそのまま委譲し、ロジック重複ゼロを維持
- **Sunset 移行**: 旧 `/v1/parking/:id/detail` を 90 日猶予で `/v1/mobile/lots/:id` に集約

---

## Architecture

- **同一 Worker**: Cloudflare Workers 単一デプロイ（`wrangler.toml`）。Worker 分割はしない。
- **routes-manifest.ts（SSoT）**: `category: "mobile"` エントリ 39 本を追加。追加・変更は routes-manifest.ts のみ。
- **サービス層共有**: `/v1/mobile/*` は既存の service 関数・DB クエリをそのまま呼ぶ。コード重複なし。
- **KV キャッシュ**: home-feed（60s TTL）・lot-detail（300s SWR）は `buildDetailCache()` パターンを踏襲。
- **OpenAPI**: `x-channels: [app-mobile]` を全 mobile operation に付与。Redoc でバッジ表示。
- **Dart クライアント**: openapi.json 再生成後に `dart run build_runner build` で自動生成。

---

## エンドポイント全覧

### Section 1: Aggregate（5本）

| Method | URL | 画面 | レスポンス shape |
|--------|-----|------|-----------------|
| GET | `/v1/mobile/boot` | Splash | `{ me, codes, app_config }` |
| GET | `/v1/mobile/home-feed` | ホームマップ | `{ lots, sponsors, active_session }` |
| GET | `/v1/mobile/profile-view` | プロフィール | `{ user, gamification: {level,xp,xp_to_next,badge_count}, subscription_status }` |
| GET | `/v1/mobile/lots/:id` | 駐車場詳細 | `{ lot, reviews, pricing_rules, nearby: {lots,sponsors}, is_open_now }` |
| GET | `/v1/mobile/premium-view` | プレミアム | `{ current_subscription, plans }` |

クエリパラメータ:
- `home-feed`: `?lat=&lng=&radius_m=`（デフォルト 8000）
- `lots/:id` の nearby: KV 300s SWR + Weak ETag

### Section 2: Thin Wrappers（34本）

#### Me / Profile 系

| Method | URL | 委譲先 |
|--------|-----|--------|
| GET | `/v1/mobile/me` | `GET /v1/me` |
| PATCH | `/v1/mobile/me` | `PATCH /v1/me` |
| POST | `/v1/mobile/me/withdraw` | `POST /v1/me/withdraw` |
| GET | `/v1/mobile/me/notification-prefs` | `GET /v1/me/notification-preferences` |
| PATCH | `/v1/mobile/me/notification-prefs` | `PATCH /v1/me/notification-preferences` |
| GET | `/v1/mobile/me/consents` | `GET /v1/me/consents` |
| PUT | `/v1/mobile/me/consents` | `PUT /v1/me/consents` |
| PUT | `/v1/mobile/me/device-permissions` | B-6 サービス関数（auth-overhaul 実装済） |

#### Push / Notifications 系

| Method | URL | 委譲先 |
|--------|-----|--------|
| PUT | `/v1/mobile/me/push-tokens` | `PUT /v1/me/push-tokens` |
| DELETE | `/v1/mobile/me/push-tokens` | `DELETE /v1/me/push-tokens` |
| GET | `/v1/mobile/me/notifications` | `GET /v1/me/notifications` |
| PATCH | `/v1/mobile/me/notifications/:id/read` | `PATCH /v1/me/notifications/:id/read` |

#### Vehicles / Presets / History 系

| Method | URL | 委譲先 |
|--------|-----|--------|
| GET / POST | `/v1/mobile/me/vehicles` | 既存 vehicles サービス |
| PATCH / DELETE | `/v1/mobile/me/vehicles/:id` | 同上 |
| GET / POST | `/v1/mobile/me/search-presets` | 既存 presets サービス |
| PATCH / DELETE | `/v1/mobile/me/search-presets/:id` | 同上 |
| GET | `/v1/mobile/me/search-history` | 既存 search-history サービス |
| GET / DELETE | `/v1/mobile/me/saved-lots` | 既存 saved-lots サービス |
| POST | `/v1/mobile/me/saved-lots/:id` | 同上 |
| GET | `/v1/mobile/me/subscription` | 既存 subscription サービス |
| POST | `/v1/mobile/me/subscription/verify-iap` | 既存 IAP verify |

#### Lots / Sessions 系

| Method | URL | 委譲先 |
|--------|-----|--------|
| GET | `/v1/mobile/lots` | `GET /v1/parking-lots`（ページネーション済） |
| POST | `/v1/mobile/lots/:id/reviews` | `POST /v1/parking-lots/:id/reviews` |
| POST | `/v1/mobile/lots/:id/calc-fee` | `POST /v1/parking-lots/:id/calc-fee` |
| POST | `/v1/mobile/sessions` | `POST /v1/parking-sessions` |
| GET | `/v1/mobile/sessions/active` | `GET /v1/me/parking-sessions?status=active` |
| GET / PATCH | `/v1/mobile/sessions/:id` | 既存 sessions サービス |
| POST | `/v1/mobile/sessions/:id/finalize` | 既存 finalize |
| POST | `/v1/mobile/sessions/:id/cancel` | 既存 cancel |

#### Misc 系

| Method | URL | 委譲先 |
|--------|-----|--------|
| GET | `/v1/mobile/subscription-plans` | 既存 plans サービス |
| POST | `/v1/mobile/search/ai` | `POST /v1/search/ai` |
| GET | `/v1/mobile/articles` | `GET /v1/articles` |
| GET / POST | `/v1/mobile/support/tickets` | 既存 support サービス |
| POST | `/v1/mobile/storage/upload-url` | 既存 storage サービス |
| GET | `/v1/mobile/codes` | `GET /v1/codes`（Edge KV キャッシュ済） |

---

## ファイル構成

```
parky/api/src/routes/v1/mobile/
├── _router.ts                  ← 全サブルーターのマウント口
├── boot.ts
├── home-feed.ts
├── profile-view.ts
├── lot-detail.ts               ← parking-detail.ts からロジック移設
├── premium-view.ts
├── lots.ts
├── sessions.ts
├── search-ai.ts
├── articles.ts
├── support.ts
├── storage.ts
├── codes.ts
├── subscription-plans.ts
└── me/
    ├── index.ts
    ├── notification-prefs.ts
    ├── consents.ts
    ├── device-permissions.ts
    ├── push-tokens.ts
    ├── notifications.ts
    ├── vehicles.ts
    ├── search-presets.ts
    ├── search-history.ts
    ├── saved-lots.ts
    └── subscription.ts
```

**既存ファイル変更（最小限）:**

| ファイル | 変更内容 |
|---------|---------|
| `routes/v1/parking-detail.ts` | `Sunset` ヘッダー（90日後）+ `Link` + `@deprecated` OpenAPI マーク |
| `routes-manifest.ts` | `category: "mobile"` 39本追加 |

---

## 実装フェーズ

### Phase 1 — Foundation（約 30 分）
- [ ] `parky/api/src/routes/v1/mobile/` ディレクトリ作成
- [ ] `_router.ts` 骨格作成（空マウント）
- [ ] `routes-manifest.ts` に `category: "mobile"` セクション追加（空 → Phase ごとに埋める）
- [ ] tsc パス確認

### Phase 2 — Aggregate 5 本（約 2 時間）
- [ ] `boot.ts`: `getMe` + `getCodes` + `getAppConfig` 並列呼び出し
- [ ] `home-feed.ts`: `getNearbyLots` + `getActiveSponsor` + `getActiveSession` 並列 + KV 60s TTL
- [ ] `profile-view.ts`: `getMe` + `getUserGamification` + `getSubscriptionStatus` 並列
- [ ] `lot-detail.ts`: `parking-detail.ts` ロジック移植 + KV 300s SWR 維持
- [ ] `premium-view.ts`: `getSubscription` + `getPlans` 並列

### Phase 3 — Sunset 移行（約 30 分）
- [ ] `parking-detail.ts` に `Sunset` ヘッダー + `Link: </v1/mobile/lots/:id>; rel="successor-version"` 追加
- [ ] OpenAPI `@deprecated` マーク追加

### Phase 4 — Me/* Thin Wrappers（約 1.5 時間）
- [ ] `me/index.ts` (GET/PATCH/withdraw)
- [ ] `me/notification-prefs.ts`
- [ ] `me/consents.ts`
- [ ] `me/device-permissions.ts`
- [ ] `me/push-tokens.ts`
- [ ] `me/notifications.ts`
- [ ] `me/vehicles.ts`
- [ ] `me/search-presets.ts`
- [ ] `me/search-history.ts`
- [ ] `me/saved-lots.ts`
- [ ] `me/subscription.ts`

### Phase 5 — その他 Thin Wrappers（約 1 時間）
- [ ] `lots.ts`
- [ ] `sessions.ts`
- [ ] `search-ai.ts`
- [ ] `articles.ts`
- [ ] `support.ts`
- [ ] `storage.ts`
- [ ] `codes.ts`
- [ ] `subscription-plans.ts`

### Phase 6 — 仕上げ（約 1 時間）
- [ ] `routes-manifest.ts` 全 39 エントリ確認
- [ ] `wrangler dev` → `GET /v1/openapi.json` 再生成
- [ ] `dart run build_runner build` で Dart クライアント再生成
- [ ] `parky/docs/mobile-partner/screen-api-map.html` の API 列を `/v1/mobile/*` に更新
- [ ] Flutter 側エンドポイント文字列を `/mobile/***` に差し替え

---

## 工数サマリー

| Phase | 作業 | 見積 |
|-------|------|------|
| 1 | Foundation | 30 分 |
| 2 | Aggregate 5 本 | 2 時間 |
| 3 | Sunset 移行 | 30 分 |
| 4 | Me/* Wrappers | 1.5 時間 |
| 5 | その他 Wrappers | 1 時間 |
| 6 | 仕上げ・OpenAPI | 1 時間 |
| **合計** | | **約 6.5 時間** |

---

## Flutter 側の変更範囲

`ParkyBffClient`（Dio ベース、`/v1` をベースパス）の各メソッドのパス文字列を `/mobile/***` に差し替えるだけ。Aggregate を利用する画面は 3→1 本への削減が可能。

```dart
// Before
final resp = await _client.get('/parking-lots', ...);

// After
final resp = await _client.get('/mobile/lots', ...);
```
