1ER図

コレクション間のリレーション(参照キーを中心に)

shops/{shopId} |-- [sub] tables/{tableNumber} | |-- tableId テーブルID(doc IDと同じ値) | |-- tableNumber テーブル番号 |-- [sub] groups/{groupId} (席セッション情報を統合) | |-- tableNumber テーブル番号 | |-- memberUids[] --> users/{uid} | |-- matchId -------> shops/{shopId}/matches/{matchId} | |-- [sub] likes/{likeId} | |-- likeId (auto-id) | |-- fromGroupId --> shops/{shopId}/groups/{groupId} | |-- fromUid -----> users/{uid} |-- [sub] matches/{sortedPairId} (ID = "{groupAId}_{groupBId}" 辞書順) |-- groupAId ----> shops/{shopId}/groups/{groupId} |-- groupBId ----> shops/{shopId}/groups/{groupId} users/{uid} (shopId を持たない。グループ所属は groups.memberUids array-contains で取得) adminAccounts/{uid} |-- shopIds[] -----------> shops/{shopId} (role='shop' の場合)

ID設計の方針: グループID は auto-id(F2)。いいねは auto-id(F3: ビジネスIDをdoc IDにせず将来の拡張に備える)。マッチは複合ID(重複防止)。サブコレクション(tables)は多店舗での番号衝突を回避(F1)。groups が席セッション管理も一元担当。

2コレクション一覧

全7コレクション。ルート / サブの分類

1 shops/{shopId} ルート

店舗マスタ。matchingPolicy・qrToken・status を保持。

2 shops/{shopId}/tables/{tableNumber} サブ

テーブルマスタ。tableId・capacity・active・label。

3 users/{uid} ルート

ユーザープロフィール。Auth UID をドキュメントID に使用。グループ所属は groups.memberUids が一次情報。

4 shops/{shopId}/groups/{groupId} サブ

グループ。auto-id。席セッション情報を統合。waiting → active → matched / expired の状態遷移(4段階)。

5 shops/{shopId}/groups/{toGroupId}/likes/{likeId} サブ

送信先グループのサブコレクション。受信いいねの取得がシンプル。doc ID = auto-id(likeId フィールドに同値を保持)。相互いいね → matches 生成。

6 shops/{shopId}/matches/{sortedPairId} サブ

マッチング成立レコード。ID は辞書順ソート済みグループID ペア。

7 adminAccounts/{uid} ルート

管理者アカウント。role: shop | master。Custom Claims と同期。

3各コレクション詳細

全7コレクションのフィールド一覧(型・説明)

1 店舗マスタ shops/{shopId} ルート

ID戦略: 手動採番(例: aoshiro-fujisawa) 用途: 店舗の基本情報・マッチングポリシー・QRトークンを管理

フィールド説明
shopIdstring店舗ID(手動採番、例: "aoshiro-fujisawa")
namestring店舗名(例: "AOSHIRO Resort 藤沢店")
addressstring住所
latnumber緯度(位置情報管理が必要な場合)
lngnumber経度(位置情報管理が必要な場合)
qrTokenstring全席共通QRに埋め込む不変トークン(推測困難なランダム値)
statusenumactive / suspended / closed
matchingPolicyobjectマッチングポリシー設定
requireOppositeGenderboolean異性グループ間のみマッチング(既定: true)
minMembersPerGroupnumberグループ最小人数(既定: 1)
maxMembersPerGroupnumber (null可)グループ最大人数(既定: null = 上限なし)
sessionTtlHoursnumberセッション有効時間(既定: 6、G-3で確定)
createdAttimestamp作成日時
updatedAttimestamp更新日時
2 テーブルマスタ shops/{shopId}/tables/{tableNumber} サブ

ID戦略: テーブル番号を文字列IDとして使用(例: "1", "2") 用途: 各店舗のテーブル設定。多店舗間の番号衝突を避けるためサブコレクション採用(F1)

フィールド説明
tableIdstringテーブルID(doc IDと同じ値、汎用参照用)
tableNumberstringテーブル番号(ユーザー手入力値と一致、例: "1", "2")
capacitynumber定員人数
activebooleanテーブルの有効/無効フラグ
labelstring表示名(例: "テーブル1番")
createdAttimestamp作成日時
updatedAttimestamp更新日時

F1: 多店舗間でテーブル番号が重複するためサブコレクション採用。

3 ユーザープロフィール users/{uid} ルート

ID戦略: Firebase Auth UID(Anonymous)をドキュメントIDとして使用(Auth と 1:1) 用途: 不変のユーザープロフィール情報を保持(5項目: name/age/gender/photoURL + uid/createdAt/updatedAt) ライフサイクル: 登録 Submit 時に作成。プロフィール更新時に updatedAt 更新。所属やステータス管理は groups コレクションで完結(groups.memberUids が単一の真実) v1差分: nickname/ageGroup/mood/wantToDo/message を廃止。さらに v2 で運用系(shopId/tableNumber/status/registeredAt/releasedAt/lastActiveAt)も廃止し、プロフィール情報+createdAt/updatedAt のみに簡素化

フィールド説明
uidstringFirebase Auth UID(Anonymous Auth、doc IDと一致)
namestring名前
agenumber年齢(数値、G-2: 年齢層ではなく実年齢)
genderenummale / female(G-2: 2択)
photoURLstringプロフィール写真のCloud Storage URL(G-6: JPEG/PNG/WebP、5MB、800x800にリサイズ)
createdAttimestamp作成日時
updatedAttimestamp更新日時

ユーザーの所属グループは groups where memberUids array-contains uid でクエリする(groups.memberUids が単一の真実の源)。店舗別ユーザーカウントは users ではなく各グループの memberUids.length の合計で算出する(A4ダッシュボード。Firestore は array-length をクエリで直接使えないため、クライアント側で集計)。

4 グループ shops/{shopId}/groups/{groupId} サブ
waiting active matched | expired

ID戦略: Firestore auto-id(F2: 可読性よりコリジョン安全性を優先) ライフサイクル: 1人目登録で waiting 生成 → グループ確定で active → マッチ成立で matched。waiting/active のまま TTL 超過で expired(G-3)。案内完了は matches.status(guided)で管理。

フィールド説明
groupIdstringグループID(auto-id)
tableNumberstring入力されたテーブル番号
statusenumwaiting / active / matched / expired
closedReasonenum (null可)終了理由: matched / manual / expired(未終了は null)
memberUidsarray<string>メンバーのUID配列(グループメンバーシップの単一の真実の源)
genderTypeenummale_only / female_only / mixed / unknown(F4: Cloud Functions で自動計算)
matchIdstring (null可)マッチ成立時のmatchID(未成立は null)
openedAttimestamp1人目登録時(グループ生成日時)
readyAttimestamp (null可)グループ確定(active)日時
matchedAttimestamp (null可)マッチ成立日時

G-3: TTL 超過で expired 化(TTL は shops.matchingPolicy.sessionTtlMinutes、既定: 60分)。G-4: マッチ成立直後の同テーブル新規登録は新規 group として生成。G-5: active 状態はメンバー追加可。matched 以降は不可。

5 グループ間いいね shops/{shopId}/groups/{toGroupId}/likes/{likeId} サブ

ID戦略: doc ID = auto-id(Firestore自動採番)。likeIdフィールドにも同値を保持(F3) ライフサイクル: いいね操作で生成 → 相互いいね成立時に matchedAt を更新

フィールド説明
likeIdstringいいねID(doc IDと同じ値、Firestore自動採番)
fromGroupIdstringいいねを送ったグループID
fromUidstringいいねを押したユーザーUID
createdAttimestampいいねした日時
matchedAttimestamp (null可)相互いいね成立日時(未成立は null)

F3: 送信先グループのサブコレクションとして配置。doc IDはauto-idで採番(ビジネスIDをdoc IDにせず、将来の再いいね等の拡張に備える)。G-7: いいね取り消しはPhase 1では不要。G-8: 受信いいね(片想い)は「あなたにいいねしたグループ」タブで表示。クエリは shops/{shopId}/groups/{自分のID}/likes を list。送信いいねは collectionGroup('likes').where('fromGroupId','==',自分のID)。マッチ成立判定はA→B作成時に逆方向 shops/{shopId}/groups/{A}/likes where fromGroupId == B を limit(1) でクエリ。

6 マッチング成立 shops/{shopId}/matches/{sortedPairId} サブ
pending_guidance guiding guided | cancelled

ID戦略: {groupAId}_{groupBId} の辞書順ソート済み複合ID(重複防止) ライフサイクル: 相互いいね成立で pending_guidance 生成 → 店員が案内開始で guiding → 案内完了で guided

フィールド説明
matchIdstringマッチID("{minGroupId}_{maxGroupId}" 辞書順ソート済み複合ID)
groupAIdstringグループAのID(辞書順で小さい方)
groupBIdstringグループBのID(辞書順で大きい方)
groupAUidsarray<string>グループAのメンバーUID配列(案内時の名簿用)
groupBUidsarray<string>グループBのメンバーUID配列(案内時の名簿用)
statusenumpending_guidance(マッチ成立)/ guiding(店員着手中)/ guided(案内完了)/ cancelled
matchedAttimestampマッチ成立日時(= 案内待ちキュー入場時刻)
guidingStartedAttimestamp (null可)店員が案内を開始した日時
guidedAttimestamp (null可)案内完了日時
cancelledAttimestamp (null可)キャンセル日時

A3 案内待ち画面クエリ: shops/{shopId}/matches where status in ['pending_guidance', 'guiding'] order by matchedAt asc。店舗横断統計は collectionGroup('matches') を使用。

7 管理者アカウント adminAccounts/{uid} ルート

ID戦略: Firebase Auth UID(Email/Password)をドキュメントIDとして使用 用途: 管理画面ログインユーザーの情報・ロール・担当店舗を管理

フィールド説明
uidstringFirebase Auth UID(Email/Password)
emailstringメールアドレス
displayNamestring表示名
roleenumshop / master
shopIdsarray<string>担当店舗IDの配列(shop ロール: 担当店舗のみ、master ロール: 空配列または ['*'])
activebooleanアカウントの有効/無効フラグ
createdAttimestamp作成日時
lastLoginAttimestamp (null可)最終ログイン日時(未ログインは null)

G-10: Custom Claims 遅延対策として、重要操作は adminAccounts ドキュメント参照で二重チェック。

4設計概要

Firebase / Cloud Firestore — v2(多店舗対応・グループいいね方式)

v1(旧設計)との主な差分: 多店舗対応・グループ概念の導入・クーポン廃止・groups による席セッション管理の一元化(1コレクション化による重複削減)・genderType フィールド導入

項目 内容
DBCloud Firestore (NoSQL)
認証(来店客)Firebase Anonymous Auth
認証(管理者)Email / Password + Custom Claims(role, shopIds)
多店舗初版から多店舗アーキテクチャ。shopId スコープ必須
登録項目名前 / 年齢 / 性別(male|female)/ 写真 / テーブル番号 の5項目
QR運用全席共通QR。テーブル番号は手入力
マッチング条件男女グループ間のみ。相互いいねで成立
席解放マッチ成立 → matched(案内完了は matches.status: guided で管理)
クーポンv2 スキーマ対象外(廃止)

コレクション数とアーキテクチャ方針

全7コレクション。サブコレクション構造を採用:

  • ルート(3): users / adminAccounts / shops(Auth UID 1:1 または店舗マスタ)
  • shops 配下サブ(3): tables / groups / matches(店舗スコープを構造で保証)。likes は groups のサブコレクションとして配置
  • groups が席セッション管理・グループ管理・ライフタイム管理を一元担当
  • 店舗横断統計(A4ダッシュボード)は collectionGroup('matches') 等を使用
  • ユーザーの所属グループは groups where memberUids array-contains uid でクエリする(groups.memberUids が単一の真実の源)