# 言問名店 全員経営営業アプリ — データモデル設計書

**作成：** 2026-06-06  
**作成者：** データ大臣 CDO  
**対象資産：** meiten-master.json（202店）/ activity.jsonl / targets.json  
**前提実装：** hq-server.py（コミット a0eaa 系）+ meiten-sales.html  
**原則：** Single Source of Truth。マスターに現在状態、イベントに変更履歴。捏造なし・実在フィールドのみ記録。

---

## 0. 設計の方針（3行サマリ）

1. **マスター = 現在の最良状態。** 状態変化（担当宣言・商談更新）はイベントを先に書き、マスターを後追いで投影する。マスターを直接書き換えたら必ずイベントも残す。
2. **イベントログ = append-only 不変。** 削除・上書きは原則禁止。訂正は `type: correction` イベントで行う。
3. **集計は導出。** achieved / pipeline / 担当別集計はすべてイベントから再計算する。スナップショットは参考値にとどめ、「正本」は計算式とみなす。

---

## 1. エンティティ一覧（ER）

### 1-1. stores — 店舗マスター（正本: meiten-master.json）

**説明：** 202店の静的属性と、多人数営業で発生する可変状態を持つ中核エンティティ。

| フィールド | 型 | PK/FK | 必須 | 説明 |
|---|---|---|---|---|
| storeId | string | PK | Y | `MTN-XXX` 連番（3桁ゼロ埋め）。採番は現行最大+1。 |
| name | string | | Y | 正式名称（店頭・公式サイトに準拠） |
| nameNormalized | string | | Y | 名寄せキー。（株）除去・全角→半角・カナ統一後の表記 |
| category | string | | Y | 業種。例 `飲食・イタリアン` `保険` |
| area.ward | string | | Y | 行政区（台東区 / 荒川区 / 港区 など） |
| area.town | string | | Y | 町丁目（根岸 / 浅草 など） |
| address | string | | Y | 住所文字列（番地まで。全角数字→半角・トリム済み） |
| lat | number | | | 緯度（WGS84）。未取得は `null` |
| lng | number | | | 経度（WGS84）。未取得は `null` |
| label | string | | Y | L1a〜L6 のセグメント分類。下記 enum 参照 |
| labelNote | string | | | labelの根拠メモ |
| issuesPlaced | number[] | | | 掲載済み号一覧（確定分のみ） |
| lastIssue | number | | | 最終掲載号 |
| status | string | | | 最新の受注状況（マスター非正規化。正本は deals/イベント） |
| contractType | string | | | 広告枠の種別（表4年契 / 言問ブランド年契 / 1/2純広告 など） |
| spendKnownTotal | number | | | 累計出稿額（請求書DB確認分のみ・推計は除く） |
| lastSpend | number | | | 最終出稿額 |
| lapsed | boolean | | Y | 途絶フラグ（true=最終掲載から2号以上空き） |
| potentialTier | string | | | 将来ポテンシャル（A/B/C） |
| tegataFit.level | string | | | 手形アプリ適性（高/中/低） |
| hotelEastFit | string | | | ホテルEast送客適性（高/中/低） |
| worldviewFit | string | | | 言問世界観適合度（定性評価） |
| nextAction | string | | | 推奨次アクション（nbaから投影・参考値） |
| googleMapsUrl | string | | | Googleマップ検索URL |
| officialSite | string | | | 公式サイトURL（不明の場合 `"不明"`） |
| rating.source | string | | | 評価元（食べログ / Yahoo!マップ など） |
| rating.score | string | | | 評価点 |
| tags | string[] | | | 検索・フィルタ用タグ（非正規化） |
| researchConfidence | string | | Y | データ信頼度 enum → §4.5 参照 |
| salesReps | SalesRep[] | | | 号別担当者履歴（下記ネスト型参照） |
| vol15 | Vol15Object | | | Vol.15 受注詳細（下記参照） |
| vol16 | Vol16Object | | | Vol.16 商談詳細（下記参照・現在状態スナップショット） |
| targetPrimary | string | | | 主要客層（地元/国内観光/インバウンド/法人） |
| targetMix | string[] | | | 複合客層（CINO分類） |
| inboundReady | string | | | インバウンド対応度（高/中/低） |
| nba | NBAObject | | | NBA（Next Best Action）スコアリング結果 |
| claimedBy | string | | | 現在の担当宣言者名。空=未宣言。（活動ログから投影） |
| claimedAt | datetime | | | 担当宣言日時（ISO 8601） |

**label enum（現行）：**

| 値 | 意味 |
|---|---|
| L1a | 継続広告主・大型純広/記事広告 |
| L1b | 継続広告主・言問ブランド年契 |
| L1c | 継続広告主・名店リスト年契 |
| L1d | 継続広告主・継続記事下 |
| L2 | 途絶・再コンタクト候補 |
| L3 | 取材実績あり・未広告化（埋蔵金） |
| L4 | グループ内（山陽建物・花重・言問East など） |
| L5 | 域外/大手 |
| L6 | 未接触の地域名店・新規開拓候補 |

**SalesRep ネスト型：**

```json
{ "rep": "宮内", "issues": [12, 13, 14], "note": "号は推定（ad-master）" }
```

**vol15 オブジェクト型：**

| フィールド | 型 | 説明 |
|---|---|---|
| status | string | 受注 / 商談中 / NG / 未着手 |
| slot | string | 掲載枠（表4 / 言問ブランド / 言問名店リスト / 1/2純広告 など） |
| amount | number | 確定金額（円） |
| rep | string | 担当者名 |
| mado | string | 担当窓口（顧客側） |
| invoiceStatus | string | 請求書状態（未発行 / 発行済 / 入金済 など） |
| invoiceKey | string | 請求書DB突合キー（`店名｜枠｜金額` 形式） |

**vol16 オブジェクト型：**

| フィールド | 型 | 説明 |
|---|---|---|
| status | string | 未着手 / 商談中 / 受注確定 / NG |
| slot | string | 予定掲載枠（未確定の場合は `"（未確定）"` 許容） |
| amount | number | 商談中=見込み額、受注確定=確定額 |
| rep | string | 担当者名 |
| mado | string | 顧客側窓口 |
| invoiceStatus | string | 未請求（商談中）/ 未発行 / 発行済 など |
| invoiceKey | string | 請求書DB突合キー（受注確定後に入力） |
| targetIssue | number | 対象号（16）|
| updatedAt | datetime | 最終更新日時（ISO 8601） |

**NBAオブジェクト型：**

| フィールド | 型 | 説明 |
|---|---|---|
| actionType | string | A01〜A0N（アクションカテゴリコード） |
| actionName | string | 推奨アクション名 |
| priority | string | P0〜P3 |
| priorityScore | number | 0〜100 の数値スコア |
| ev | number | 期待値（円）= p_convert × v_expected × s_strategic − c_action |
| repRecommended | string | 推奨担当者 |
| deadlineDays | number | 行動期限（日数） |
| deadlineDate | date | 行動期限（日付） |
| confidence | string | 確度メモ（推計/確認済 など） |
| generatedAt | datetime | NBA計算日時 |

---

### 1-2. users — 担当者マスター（正本: mvp/source/担当者マスター.json）

**説明：** 営業担当12名の識別子と組織。アプリの「誰が操作したか」の解決源。

| フィールド | 型 | PK/FK | 必須 | 説明 |
|---|---|---|---|---|
| userId | string | PK | Y | 担当者名を主キーとして流用（例: `宮内`）。将来のID化に備え `userId` フィールドで抽象化 |
| displayName | string | | Y | 表示名（例: `宮内`） |
| supervisor | string | FK→users.userId | | 上長（例: `石井`）。上長自身の場合は自分を指す or null |
| role | string | | Y | `rep`（営業）/ `admin`（編集長・CEO）|

**現行担当者リスト（担当者マスター.json から）：**
阿部 / 石井 / 金井 / 加藤 / 満田 / 宮内 / 杉本 + 山田（CEO）

**設計注：** 現行の `salesReps[].rep` フィールドは「宮内（杉本）」「宮内（加藤）」など `上長（担当）` 形式で複合表記されている。これは _複数人チーム担当_ を示す非正規化表現。usersマスターでは `displayName=宮内` の1レコードとして管理し、チーム担当の細部は `activities.payload.subRep` で補完する。

---

### 1-3. claims — 担当宣言（正本: activity.jsonl + Googleシート claims）

**説明：** 誰がどの店を担当しているかの現在状態。activity.jsonl の `type: "claim"` イベントから導出するが、アプリ高速読み取りのためスナップショットをシートに保持する。

| フィールド | 型 | PK/FK | 必須 | 説明 |
|---|---|---|---|---|
| claimId | string | PK | Y | UUID（クライアント生成）|
| storeId | string | FK→stores.storeId | Y | 対象店 |
| userId | string | FK→users.userId | Y | 宣言者 |
| claimedAt | datetime | | Y | 宣言日時（ISO 8601） |
| active | boolean | | Y | true=有効な宣言。1店につき active=true は最大1件 |
| releasedAt | datetime | | | 解除日時（active=false 時） |
| releasedBy | string | FK→users.userId | | 解除者（本人以外が解除した場合はAdminの userId） |

**担当宣言ロックルール（整合性）：**
- `SELECT active=true WHERE storeId=X` が0件のとき のみ `claim` 操作を許可する。
- 上記チェックは hq-server.py がトランザクション的に実行する（Sheets 書込は行単位なので楽観ロック相当）。
- 競合解決：2人が同時に宣言した場合、Sheets への書込は後者が勝つ。アプリは `/api/claim` レスポンスに `{ ok: true, claimedBy: "..." }` を返し、フロントが反映後に再確認する。30秒ポーリングで矛盾は自然解消する。

---

### 1-4. activities — 活動イベントログ（正本: data/activity.jsonl）

**説明：** 全員の操作履歴・フィード・監査の唯一の源。append-only。削除禁止。

| フィールド | 型 | PK/FK | 必須 | 説明 |
|---|---|---|---|---|
| id | string | PK | Y | UUID。現行は ts+user の複合で一意性確保（UUID化を推奨） |
| ts | datetime | | Y | 発生日時（ISO 8601・タイムゾーン付き）|
| userId | string | FK→users.userId | Y | 操作者（`user` フィールドを `userId` にリネーム推奨） |
| storeId | string | FK→stores.storeId | Y | 対象店 |
| storeName | string | | | 非正規化・フィード表示用 |
| type | string | | Y | イベント種別 enum → 下記参照 |
| payload | object | | | 種別ごとの可変データ（下記参照） |

**type enum（現行実態 + 推奨拡張）：**

| 値 | 意味 | payload フィールド |
|---|---|---|
| `claim` | 担当宣言 | `{ action: "claim" or "unclaim" }` |
| `挨拶` | 挨拶訪問 | `{ memo, nextAction, nextActionDate }` |
| `訪問` | 訪問記録 | `{ memo, nextAction, nextActionDate }` |
| `商談` | 商談記録 | `{ memo, nextAction, nextActionDate }` |
| `受注` | 受注報告 | `{ memo, amount }` |
| `情報` | 情報追加（field intel） | `{ note }` |
| `電話` | 電話記録 | `{ memo, nextAction }` |
| `deal` | 商談状態・金額の変更 | `{ status, amount }` |
| `addinfo` | 追加情報 | `{ note }` |
| `correction` | 訂正（過去イベントの誤りを打ち消す） | `{ targetId: <訂正対象イベントのid>, reason, correctedBy }` |

**設計注：** 現行 activity.jsonl は `type` に日本語（`挨拶` `受注` `addinfo` `deal` `claim`）が混在している。将来的には英字コードに統一することを推奨するが、現行UIとの互換を優先し、現行フィールド名を維持した上で `type` の値として日本語も許容する。

**訂正の扱い：**
- 過去イベントは削除しない。`type: "correction"` イベントを新規追加し、`payload.targetId` で訂正対象を指す。
- 集計・表示時は `correction` イベントが存在するイベントを「無効」として扱う。

---

### 1-5. deals — 号別商談スナップショット（正本: Googleシート vol16_deals）

**説明：** 各店のVolXX商談の現在状態。activityの `type: "deal"` イベントから派生するが、集計速度のためシートにスナップショットを保持する。

| フィールド | 型 | PK/FK | 必須 | 説明 |
|---|---|---|---|---|
| storeId | string | FK→stores.storeId | Y | 対象店 |
| issue | number | | Y | 対象号（15 / 16 / 17…） |
| storeName | string | | | 非正規化 |
| rep | string | FK→users.userId | | 担当者名 |
| status | string | | Y | `未着手` / `商談中` / `受注確定` / `NG` |
| amount | number | | | 受注確定時の確定金額（円） |
| pipelineAmount | number | | | 商談中の見込み金額（円） |
| slot | string | | | 掲載枠（表4 / 言問ブランド など） |
| updatedAt | datetime | | Y | 最終更新日時 |
| updatedBy | string | FK→users.userId | Y | 更新者 |

**複合PK：** `(storeId, issue)` の組み合わせで一意。同一店×同一号のレコードは1件のみ。

---

### 1-6. issues_targets — 号別売上目標（正本: data/targets.json）

**説明：** 号単位の目標・実績スナップショット。

| フィールド | 型 | PK/FK | 必須 | 説明 |
|---|---|---|---|---|
| issue | number | PK | Y | 号番号（15 / 16 / 17…） |
| label | string | | Y | 号のラベル（例: `Vol.16 秋号`） |
| target | number | | Y | 売上目標（円） |
| targetIsProposal | boolean | | | true=提案値（CEO未確定） |
| achievedConfirmed | number | | Y | 確認済み受注額（円）。初期値 `0` |
| pipelineNegotiating | number | | | 商談中見込み（円） |
| pipelineUnstarted | number | | | 未着手で見込みがある案件（円） |
| status | string | | | 号の現在フェーズ（例: `営業始動` `発刊準備`） |

---

### 1-7. news_clips — ニュースクリップ（正本: mvp/news_clips.csv → Googleシート）

| フィールド | 型 | PK/FK | 必須 | 説明 |
|---|---|---|---|---|
| clipId | string | PK | Y | UUID |
| storeId | string | FK→stores.storeId | | 関連店（任意。地域全般情報は null） |
| storeName | string | | | 非正規化 |
| publishedAt | date | | | 記事掲載日 |
| source | string | | Y | 媒体名 |
| url | string | | | 記事URL |
| headline | string | | Y | 見出し |
| summary | string | | | 要約（AIが入力可） |
| tags | string[] | | | タグ |
| addedBy | string | FK→users.userId | Y | 登録者（`AI` も許容） |
| createdAt | datetime | | Y | 登録日時 |

---

### 1-8. field_intel — フィールド情報（正本: mvp/field_intel.csv → Googleシート）

| フィールド | 型 | PK/FK | 必須 | 説明 |
|---|---|---|---|---|
| intelId | string | PK | Y | UUID |
| storeId | string | FK→stores.storeId | | 関連店（任意） |
| storeName | string | | | 非正規化 |
| userId | string | FK→users.userId | Y | 投稿者 |
| intelAt | datetime | | Y | 情報取得日時 |
| source | string | | | 情報源（口頭 / 来店 / SNS / 現地観察 など） |
| content | string | | Y | 内容（自由記述） |
| relevance | string | | | 関連性タグ（広告可能性 / 閉店懸念 / イベント など） |
| createdAt | datetime | | Y | 登録日時 |

---

### 1-9. visit_logs — 訪問記録（正本: mvp/visit_logs.csv → Googleシート）

**説明：** field_intel の書式化版。activities との重複に見えるが、写真URL・次アクション期日など _訪問記録_ 固有のフィールドを持つため分離する。将来的に activities へ統合することも可だが、現行は別テーブルとして維持する。

| フィールド | 型 | PK/FK | 必須 | 説明 |
|---|---|---|---|---|
| visitId | string | PK | Y | UUID（クライアント生成） |
| storeId | string | FK→stores.storeId | Y | 対象店 |
| storeName | string | | | 非正規化 |
| userId | string | FK→users.userId | Y | 担当者 |
| visitedAt | datetime | | Y | アクション日時 |
| type | string | | Y | 挨拶 / 訪問 / 商談 / 受注 / 情報 / 電話 |
| summary | string | | | 自由メモ |
| photoUrl | string | | | 写真URL（Google Drive） |
| nextAction | string | | | 次アクション（任意） |
| nextActionDate | date | | | 期日（任意） |
| vol16statusChange | string | | | このアクションでvol16 statusが変わった場合の新status |
| vol16amountChange | number | | | このアクションでvol16 amountが変わった場合の新amount |
| createdAt | datetime | | Y | 記録日時 |

---

## 2. カーディナリティ（ER図）

```
users (1) ─────< activities (N)    : 1担当者が複数イベントを生成
users (1) ─────< claims (N)        : 1担当者が複数店を担当可能（過去含む）
users (1) ─────< deals (N)         : 1担当者が複数商談を更新
users (1) ─────< visit_logs (N)    : 1担当者が複数訪問記録
users (1) ─────< field_intel (N)   : 1担当者が複数情報を投稿

stores (1) ────< activities (N)    : 1店舗に複数イベント
stores (1) ────< claims (0..1)     : 1店舗につきactive=trueのclaimは最大1件
stores (1) ────< deals (N)         : 1店舗に複数号の商談（issues_targets.issue で号別）
stores (1) ────< visit_logs (N)    : 1店舗に複数訪問記録
stores (1) ────< field_intel (N)   : 1店舗に複数フィールド情報
stores (1) ────< news_clips (N)    : 1店舗に複数ニュース（storeId が null も可）

issues_targets (1) ─< deals (N)   : 1号に複数商談
```

---

## 3. Single Source of Truth（真実源の所在）

| データ | 正本（SSOT） | 派生・非正規化 | 整合保証方法 |
|---|---|---|---|
| 店舗属性（名称・住所・label・nba等） | meiten-master.json | stores.csv（エクスポート）・Googleシート stores タブ | JSON→CSV 変換スクリプトで一方向同期。Sheets を編集したら必ず JSON に反映する手順を運用ルール化 |
| 担当宣言の現在状態 | activities（activity.jsonl） | stores.claimedBy / claims シート | hq-server.py が `claim` イベント書込後に claims シートと stores.claimedBy を更新する |
| Vol.16 商談状態 | activities（activity.jsonl）の `deal` イベント列 | stores.vol16 / deals シート | `deal` イベント書込後に deals シートと stores.vol16.status/amount を更新する |
| Vol.16 集計（achieved/pipeline/remaining） | deals シートからの再計算 | targets.json（提案値のみ）| `/api/vol16-summary` が都度 deals を集計して返す。キャッシュは30秒 |
| 担当者マスター | mvp/source/担当者マスター.json | セレクトボックス選択肢 | 手動管理（担当者変更時に JSON を更新） |
| 号別目標 | data/targets.json | ダッシュボード表示 | CEO確定値のみ `targets.json` に書く。提案値は `targetIsProposal: true` で明示 |

**原則：** stores.vol16 と deals シートは「表示高速化のための冗長コピー」であり、イベントログが正本である。矛盾が生じたらイベントログから再生成する。

---

## 4. 整合性・並行制御

### 4-1. 担当宣言の重複防止

```
POST /api/claim { storeId: "MTN-063", rep: "阿部", action: "claim" }

サーバー処理:
1. claims シートで active=true AND storeId=MTN-063 の行を検索
2. 存在する → { ok: false, error: "既に阿部が担当中", claimedBy: "阿部" } を返す
3. 存在しない → claims シートに新行追記（active=true）& stores の claimedBy を更新
4. レスポンス: { ok: true, claimedBy: "阿部", claimedAt: "<ts>" }
```

同時書込の競合（楽観ロック）：Googleシートは行単位の書込。ミリ秒差で2人が同時に `claim` を叩いた場合、後から書いた方が勝つ。実害は「一瞬だけ2人が担当になる」状態で、30秒ポーリングで自然解消する。担当12名の実運用では許容範囲。

### 4-2. deals の重複防止（同一storeId×issue）

```
POST /api/vol16-update { storeId: "MTN-063", status: "商談中", ... }

サーバー処理:
1. deals シートで storeId=MTN-063 AND issue=16 の行を検索
2. 存在する → その行を上書き更新（updatedAt/updatedBy を更新）
3. 存在しない → 新行追記
4. 集計: achieved / pipeline / remaining を再計算して返す
```

### 4-3. enum 制約

実装上は hq-server.py の POST ハンドラで `status` の値を検証する（無効値は 400 を返す）。

| フィールド | 許容値 |
|---|---|
| deals.status | `未着手` / `商談中` / `受注確定` / `NG` |
| activities.type | `claim` / `挨拶` / `訪問` / `商談` / `受注` / `情報` / `電話` / `deal` / `addinfo` / `correction` |
| users.role | `rep` / `admin` |
| stores.researchConfidence | `確認済` / `確認済（再洗い）` / `確認済（公式サイト）` / `確認済（公式/エキテン/食べログ）` / `要ヒアリング` / `ベスト100(未接触)` / `新規(15号)` |

### 4-4. 必須フィールドの検証

hq-server.py の `visit-log` / `vol16-update` エンドポイントは以下を必須チェックする：

- `storeId`：MTN-XXX 形式 かつ meiten-master.json に存在すること
- `userId`（現行 `rep`）：担当者マスターに存在すること
- `ts` / `visitedAt`：ISO 8601 形式

---

## 5. 集計導出ルール

### 5-1. Vol.16 売上集計（最重要）

```python
# イベントログからの再計算（正本計算式）

# ① deals シート（スナップショット）から集計（高速・通常時）
achieved    = SUM( deals WHERE issue=16 AND status='受注確定' → amount )
pipeline    = SUM( deals WHERE issue=16 AND status='商談中'   → pipelineAmount )
remaining   = targets['16']['target'] - achieved
reach_rate  = achieved / targets['16']['target'] * 100

# ② activity.jsonl からの再計算（監査・矛盾確認時）
# 各 storeId×issue=16 の最新 deal イベントを取得
# → achieved / pipeline を同式で算出し、①と比較する
```

### 5-2. 担当者別集計

```python
# deals シートから
by_rep = {}
for row in deals WHERE issue=16:
    rep = row['rep']
    if row['status'] == '受注確定':
        by_rep[rep]['achieved'] += row['amount']
    elif row['status'] == '商談中':
        by_rep[rep]['pipeline'] += row['pipelineAmount']
```

### 5-3. 店舗の最終接触日・接触件数

```python
# visit_logs / activities から
for store in stores:
    logs = [v for v in visit_logs if v['storeId'] == store['storeId']]
    store['lastVisitDate'] = max(v['visitedAt'] for v in logs) if logs else None
    store['visitCount']    = len(logs)
```

### 5-4. スナップショット vs 再計算の使い分け

| 用途 | 方式 | 根拠 |
|---|---|---|
| ダッシュボード表示（通常） | deals シートからの集計（スナップショット） | 速度優先。30秒ポーリングで鮮度担保 |
| 月次経営報告・請求書確認 | イベントログからの完全再計算 | 正確性優先。deals シートの誤りも検出できる |
| 監査・不整合検出 | 両方を計算して比較 | 乖離がある場合はイベントログが正 |

---

## 6. 監査・トレーサビリティ

### 6-1. 全イベントにユーザー+タイムスタンプ

activity.jsonl の全レコードに `userId`（現行 `user`）と `ts` が必須。これにより「誰がいつ何をしたか」の完全な監査証跡が残る。

### 6-2. 訂正の手順

**禁止：** 過去のイベントレコードを直接編集・削除すること。

**正しい手順：**
```json
{
  "id": "uuid-new",
  "ts": "2026-06-10T10:00:00+09:00",
  "userId": "石井",
  "storeId": "MTN-063",
  "type": "correction",
  "payload": {
    "targetId": "uuid-of-wrong-event",
    "reason": "金額入力ミス（50000→30000）",
    "correctedBy": "石井"
  }
}
```

集計ロジックは `correction` で指定された `targetId` のイベントを無効とみなす。

### 6-3. deals の更新履歴

deals シートは「最新状態の上書き」のため変更履歴は失われる。変更履歴は activity.jsonl の `deal` イベントが保持するため、監査時はイベントログを参照する。

---

## 7. 拡張性

### 7-1. 号（issue）の追加

targets.json に新しい号を追加し、deals シートの `issue` フィールドに新しい号番号を使うだけで対応する。スキーマ変更不要。

```json
// targets.json への追記例（Vol.17）
"17": {
  "label": "Vol.17 春号",
  "target": 1900000,
  "targetIsProposal": true,
  "achievedConfirmed": 0,
  "status": "準備前"
}
```

### 7-2. 新規タグ・客層の追加

`stores.tags`（string[]）と `stores.targetMix`（string[]）は配列形式のため、新しい値を追加するだけで拡張できる。既存レコードへの影響なし。

### 7-3. 新しい活動タイプの追加

activity.jsonl の `type` enum に新しい値を追加し、UI に対応する選択肢を追加するだけで拡張できる。既存データへの影響なし。

### 7-4. Googleシート / Glide へのミラー対応スキーマ表

| JSON フィールド | Sheets 列名 | Glide フィールド名 | 備考 |
|---|---|---|---|
| storeId | storeId | Store ID | PK |
| name | name | Name | |
| category | category | Category | |
| area.ward | ward | Ward | ネスト→フラット展開 |
| area.town | town | Town | |
| address | address | Address | |
| label | label | Label | |
| nba.priority | 優先度 | Priority | |
| nba.priorityScore | 優先度スコア | Priority Score | |
| vol16.status | vol16status | Vol16 Status | |
| vol16.amount | vol16amount | Vol16 Amount | |
| claimedBy | claimedBy | Claimed By | |
| claimedAt | claimedAt | Claimed At | |
| tags | tags | Tags | 配列→セミコロン区切り文字列 |

Sheets への移行時は `merge_integration.py` を参考に `area.ward` / `area.town` などネスト構造をフラット化するスクリプトを用意する（`export_for_sheet.py` が原型）。

---

## 8. データ品質・現状のアラート

### 8-1. 要確認14店（researchConfidence = "要ヒアリング"）

以下の14店は住所・業種・営業状況が未確認。アプリ運用開始前に担当者が現地確認またはヒアリングで情報を埋める。

| storeId | 店名 | 主な不明点 |
|---|---|---|
| MTN-017 | BAR U BLUE | 住所（根岸1-3-20 2F）・正式店名・営業状況 |
| MTN-021 | とんかつ とん将 | 要ヒアリング |
| MTN-026 | カフェバーミヤミ | 要ヒアリング |
| MTN-027 | 中国菜〜道 dao〜 | 要ヒアリング |
| MTN-044 | セレッソ | 業種不明・住所なし |
| MTN-053 | Braceria La Fame | 住所（台東区根岸まで）番地不明 |
| MTN-064 | ふくろう屋 | 業種不明 |
| MTN-065 | 萩乃ビール | 要ヒアリング |
| MTN-068 | ふらみんご保険事務所 | 要ヒアリング（vol16 商談中だが基本情報未確認） |
| MTN-069 | 台東区医師会 | 要ヒアリング |
| MTN-073 | カーブス（上野） | 要ヒアリング |
| MTN-075 | コミヤ電気 | 要ヒアリング（vol16 商談中） |
| MTN-088 | ヤクルト（台東区拠点） | 要ヒアリング（vol16 未着手） |
| MTN-105 | 国動物クリニック | 要ヒアリング |

**優先度：** vol16 フィールドを持つ MTN-068・MTN-075・MTN-088 は商談進行中のため最優先で情報補完する。

### 8-2. storeId 採番ルール

- 形式：`MTN-XXX`（3桁ゼロ埋め・連番）
- 現行最大：MTN-202 相当（202件）
- 新規採番：既存 storeId の最大値 +1。採番前に nameNormalized と address の一致チェックを行い、既存店との重複がないことを確認する。

### 8-3. 名寄せキー

重複判定の基準：

1. **一致判定（確実な同一）：** `nameNormalized` が完全一致 かつ 同一住所（番地まで）
2. **候補判定（要確認）：** `nameNormalized` の編集距離 ≤ 2 かつ 同一町丁目（area.town が一致）
3. **住所のみ一致：** 同一住所に複数の nameNormalized → 同一ビルの別テナントの可能性。目視確認を必須とする。

`normalize.py validate` 相当の処理を定期実行し、候補一覧を CEO または編集長に提示すること。

### 8-4. 住所の正規化ルール

- 全角数字→半角（例：`２４` → `24`）
- 丁目表記統一（例：`2−24−1` / `2-24-1` / `2丁目24番1号` → `2-24-1`）
- 前後空白除去（`trim()`）
- 「東京都台東区」は補完可（省略形 `台東区根岸1-3-20` も許容。Geocode時に補完）

---

## 9. バックアップ・復旧

### 9-1. 二重保護の構造

```
[正本 1] meiten-master.json
  ├── Git で差分管理（HQ company リポジトリ）
  ├── OneDrive 経由で Mac mini にミラー（10分間隔）
  └── 破損時: Git から直前コミットを checkout で即復元

[正本 2] data/activity.jsonl（append-only）
  ├── Git で差分管理（1行追加のコミットが蓄積）
  ├── OneDrive ミラー
  └── 破損時: Git から復元。書込喪失分は Googleシートの deals/claims から逆算

[正本 3] Googleスプレッドシート
  ├── Google Drive の自動バージョン履歴（30日間）
  └── 破損時: Drive の「変更履歴を確認」から以前のバージョンを復元
```

### 9-2. activity.jsonl の破損復元手順

1. Git log で直前の正常コミットを特定：`git log --oneline data/activity.jsonl`
2. 復元：`git checkout <commit> -- data/activity.jsonl`
3. 喪失したイベントを Googleシートの deals/claims スナップショットから手動補完する

### 9-3. meiten-master.json の破損復元手順

1. Git から復元：`git checkout HEAD -- data/meiten-master.json`
2. 最新状態との差分を確認し、失われた更新（nba再スコアリング等）を再実行する

---

## 10. 現行フィールド名との対応（命名改善提案）

現行実装との互換を維持しつつ、将来のリファクタ時に考慮すべき改善案。

| 現行フィールド名 | 推奨フィールド名 | 理由 |
|---|---|---|
| `user` （activity.jsonl） | `userId` | users マスターとの FK を明示 |
| `vol16status` （stores） | `vol16.status` | stores の vol16 オブジェクトに集約 |
| `vol16amount` （stores） | `vol16.amount` | 同上 |
| `rep` （visit_logs・claims） | `userId` | users マスターとの FK を明示 |
| `visitedAt` （visit_logs） | `activityAt` | visit_logs が activity に統合される将来に備え |

**方針：** フィールド名の変更は破壊的変更（APIレスポンス・フロントのキー参照が全変更）のため、現在の実装（a0eaa 系）には適用しない。次の大型リファクタ（Phase 2 以降）で適用する。現行は上記を「技術的負債メモ」として記録しておく。

---

## 11. CEO・実装者が従うべき要点（チェックリスト）

### CEOが確認・決定すること

- [ ] Vol.16 目標 ¥1,800,000 の確定（現行 `targetIsProposal: true`）
- [ ] 担当者マスターの最終確認（12名リストの確定）
- [ ] 要ヒアリング14店のうち vol16 商談中3店（MTN-068・075・088）の情報補完指示

### 実装者（CTO・Codex）が守ること

1. **POST エンドポイントは必ず activity.jsonl に先に書いてから** deals シート・stores を更新する。イベントが正本であることをコードで保証する。
2. `storeId` は PUT/PATCH で変更不可。変更は `type: "correction"` イベントで行う。
3. claims シートの `active=true` は 1 storeId につき 1 件まで。追加前に必ず既存チェックを行う。
4. deals シートの PK は `(storeId, issue)` の複合。同じ組み合わせが2行存在したら即アラートを出す。
5. activity.jsonl への書込は追記のみ（`open(path, 'a')`）。既存行を絶対に編集しない。
6. `targets.json` の `target` 値は CEO 確定後のみ変更する。`targetIsProposal: true` の間は変更禁止。

### データ大臣が定期実行すること

- 月次：meiten-master.json の重複候補スキャン（nameNormalized 編集距離 ≤ 2）
- 月次：deals シートと activity.jsonl の集計値突合（乖離があればイベントログで修正）
- 週次：要ヒアリング店の補完状況確認
- 号クローズ時：achievedConfirmed を deals の受注確定合計で確定更新し、targets.json に反映

---

*データモデル設計書 v1.0 — データ大臣 CDO / 2026-06-06*  
*進行中の hq-server 実装（a0eaa）・meiten-sales.html と矛盾しないことを確認済み。実在フィールドのみ記録・捏造なし。*
