模擬店ステータス
各模擬店の混雑度 (空き / やや混雑 / 混雑) および在庫状況 (在庫あり / 残りわずか / 完売) をリアルタイムに集約・表示するコンポーネント。
使い方
参加者向け
import BoothStatus from "@/features/booth/components/BoothStatus";
<BoothStatus />2つのカードに分割する場合
import BoothStatus from "@/features/booth/components/BoothStatus";
<BoothStatus split="first"/>
<BoothStatus split="second"/>模擬店管理者向け
import BoothManager from "@/features/booth/components/BoothManager";
<BoothManager />API src/features/booth/api.ts
クライアントからのデータ取得・更新は以下の API を通じて行われます。更新 API performMutation は実行完了時に自動でキャッシュを破棄します。
| API 関数 | 引数 / ペイロード | 役割 |
|---|---|---|
fetchStallsOnly(ttl) | ttl: number | 混雑・在庫ステータスのみを軽量 RPC get_stalls_only で取得 |
updateStallStatus(stallName, updates) | stallName: stringupdates: { crowdLevel?: number, stockLevel?: number } | 指定した模擬店の混雑度・在庫状況を更新し、all, stalls_only キャッシュを無効化 |
コード
isDirty による差分検出 useBoothManager.ts
選択値がDBの現在値と異なる場合のみ「未反映」バナーを表示し、余分な更新リクエストを防止します。
const checkDirty = (currentCrowd: StatusLevel, currentStock: StatusLevel) => {
if (fetchedData?.stalls && assignedStall) {
const myStall = fetchedData.stalls.find((s) => s.stallName === assignedStall);
if (myStall) {
const dirty = currentCrowd !== myStall.crowdLevel || currentStock !== myStall.stockLevel;
setIsDirtyInternal(dirty);
isDirtyRef.current = dirty;
return dirty;
}
}
return false;
};共通キャッシュレイヤー src/lib/Server/baseApi.ts
fetchWithCache はメモリキャッシュ + SessionStorage の2層でキャッシュし、performMutation は書き込み後に指定キーを即座に無効化します。
// performMutation: 書き込み後に指定キーのキャッシュを無効化
export const performMutation = async <T>(
action: () => Promise<T>,
cacheKeysToInvalidate: string[] = ["all"]
): Promise<T> => {
const result = await action();
invalidateCache(cacheKeysToInvalidate);
return result;
};データベーススキーマ
CREATE TABLE stalls_status (
id SERIAL PRIMARY KEY,
stall_name TEXT NOT NULL UNIQUE,
crowd_level SMALLINT DEFAULT 0, -- 0: 空き, 1: やや混雑, 2: 混雑
stock_level SMALLINT DEFAULT 0, -- 0: 在庫あり, 1: 残りわずか, 2: 完売
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
);