Appearance
当日運営フロー仕様 (スコア・順位確定)
📌 目的: 当日運営のうち スコア入力 / 順位算出 / 順位確定 周りの現状実装を整理し、 ポーリング・キャッシュ更新の挙動を明文化する。Issue #164 の拡張議論の土台にする。
1. 関係者と画面
| 役割 | 主に触る画面 | 権限 (manage_features) |
|---|---|---|
| スタッフ (現場担当) | /staff/[tid]/... (スマホ最適化) | scores: edit / checkin: edit / tournaments: view |
| 運営 (admin/editor) | /manage/(gated)/tournaments/[id]/... | tournaments: edit (= スコアロック・順位確定権限あり) |
| 観戦者 (参加者・一般) | /tournaments/[id]/timetable | 認証不要・読み取りのみ |
2. 当日タイムライン
mermaid
sequenceDiagram
autonumber
participant Player as 参加者
participant Staff as スタッフ
participant API as API
participant DB as DB
participant Spectator as 観戦者
Note over Staff,DB: 受付 (大会前)
Player ->> Staff: 来場
Staff ->> API: PUT /manage/checkin/:tid/registrations/:rid
API ->> DB: registrations.checked_in_at = now()
Note over Staff,DB: 予選試合
loop 予選 全試合
Staff ->> API: PUT /manage/scores/:tid/matches/:mid/score<br/>(homeScore, awayScore, status=done)
API ->> DB: timetable_matches を更新
end
Note over API,DB: ★ 予選全試合 done → 自動で順位算出
Spectator ->> API: GET /tournaments/:id/timetable
API ->> DB: SELECT matches, registrations
API ->> API: loadStandings() で順位を計算<br/>resolveBracket() で決勝Tのチーム名解決
API -->> Spectator: 順位反映済タイムテーブル
Note over Staff,DB: 決勝T
loop 決勝T 全試合
Staff ->> API: PUT /manage/scores/:tid/matches/:mid/score
API ->> DB: timetable_matches を更新
end
Note over Staff,DB: 大会終了
Staff ->> API: POST /manage/tournaments/:id/finalize-standings (★Issue #164)
API ->> DB: tournaments.status = 'done'
API ->> DB: standings snapshot 保存 (TBD)3. スコア入力フロー
3.1 入力 UI (ScoringClient.tsx)
スタッフが1試合ずつ操作する5フェーズ状態マシン:
ready (未開始)
│ Startボタン
▼
live (進行中) ⇄ paused (一時停止)
│ End ボタン
▼
ended (終了確認)
│ 送信ボタン
▼
submitted (送信済)タイマーは Client 内 setInterval(1秒)、サーバには送信しない。
3.2 送信先 API
PUT /manage/scores/:tid/matches/:mid/score
| 項目 | 内容 |
|---|---|
| 認可 | scores: edit (= staff も書ける) |
| ロック中 | scoreLocked=true の試合は tournaments: edit (= 運営) のみ上書き可。staff は 409 SCORE_LOCKED |
| 更新内容 | timetable_matches.{homeScore, awayScore, status} を書き換え |
| 副作用 | 無し (順位の再計算・大会ステータス変更はしない) |
3.3 ロック / 解除 (運営のみ)
PUT /manage/scores/:tid/matches/:mid/lock (tournaments: edit 必須)
スタッフ入力後、運営が scoreLocked=true で「確定」する想定。 ロックされた試合はスタッフから再編集できない。
4. 順位算出フロー
4.1 リアルタイム計算 (loadStandings)
apps/api/src/timetable/load-standings.ts
- 呼び出されるたびに
timetable_matchesから DB 取得 →computeStandings()で順位を計算 - キャッシュなし、保存もなし、純関数的
- 戻り値: グループごとの順位行 +
complete(そのグループの予選が全試合スコア確定済か)
4.2 決勝T のチーム解決 (resolveBracket)
apps/api/src/timetable/resolve-bracket.ts
- 予選 standings + groupComplete + KO 試合の結果 → KO 試合の
home/awayを 実チーム名 に解決 groupComplete=trueの予選のみ実チーム解決 (途中順位での誤確定を避ける)- 未確定なら
1位の勝者等のプレースホルダのまま
4.3 呼び出し動線
| 画面 | 経由 API | 順位/KO 解決 | 表示 |
|---|---|---|---|
観戦者: /tournaments/:id/timetable | GET /public/timetable/:id | ✅ resolveBracket 通過 | 解決済表示 |
運営: /manage/.../prep/TimetableMatchesPanel | GET /manage/timetable/:tid/... | ✅ resolveBracket 通過 | 解決済表示 |
運営: /manage/.../tournaments/:id/standings (順位タブ) | GET /manage/timetable/:tid/standings | loadStandings のみ | 順位表 |
スタッフ: /staff/[tid] (試合一覧) | GET /manage/timetable/:tid/matches | ❓ 要確認 (未調査) | 試合一覧 |
⚠️ 要検証: スタッフ画面
/staff/[tid]で決勝T のチーム名が1位の勝者のままになる という報告が来ている (Issue #164 拡張議論)。API は解決済みデータを返しているはずなので、 UI 側でキャッシュ・取得経路に問題がある可能性が高い。
5. 状態の再取得 (MVP は手動更新のみ)
5.1 方針
MVP では 自動ポーリングを行わない。スタッフ / 運営の操作画面に <ManualRefresh /> (最終取得時刻 + 更新ボタン) を配置し、 ユーザが任意のタイミングで最新状態に追いつく。
過去には
<AutoRefresh intervalMs={5000} />で5秒ポーリングしていたが、 Netlify Functions Invocations と Cloud SQL 接続数の上限リスクから MVP では除去 (PR #169 / Issue #168)。AutoRefreshコンポーネント本体は Post-MVP の 再導入余地として残置。
5.2 <ManualRefresh /> (汎用)
apps/web/src/components/ManualRefresh.tsx
- Client Component。
useTransition+router.refresh()でサーバコンポーネントを再取得 - 「最終取得 HH:MM:SS [↻]」を表示する pill 型ボタン
- 初回 render は
--:--:--で SSR/Client hydration mismatch を回避 useEffect([pending])でpendingが false に戻った瞬間 = サーバ再取得完了時刻 を記録- アクセシブルネームは可視テキスト +
sr-onlyで構成 (スクリーンリーダーにも時刻が伝わる)
5.3 使用箇所
| 画面 | 用途 |
|---|---|
/staff/[tid]/page.tsx (試合一覧) | 他スタッフのスコア確定を取り込む |
/staff/[tid]/checkin/CheckinMobile.tsx (受付) | 他端末の受付を取り込む |
manage/.../event-day/check-in/CheckinBoard.tsx (受付) | 同上 (運営側端末) |
5.4 即時反映が必要な動線 (自分の操作)
- 自分の操作 (スコア入力・受付トグル・現金受領記録) は
revalidatePathで Server Action の中から即時再取得される。手動更新を待たずに自分の画面には反映される - 「他端末の更新を反映する」だけが ManualRefresh の用途
5.5 Post-MVP で再導入する場合の指針
- 採用候補: SWR / TanStack Query のクライアントポーリング (個別エンドポイント単位、SSR を介さない)、または SSE
- 引き戻しの判断材料: スケール (~100大会/月超え) + Netlify Functions / Cloud SQL の実測メトリクス
- 当時の試算 (参考): 5秒 × 5端末 × 3時間 = 約 10,800 req/大会。月220大会で Netlify Pro 枠超過開始、Cloud SQL 接続枯渇懸念
6. 順位確定 / 大会終了周り (現状)
6.1 現状ステータス遷移
apps/api/src/manage/tournament-status.ts
draft ⇄ recruiting ⇄ full
│
▼
done (= 終了する手動ボタン)
│
▼
(戻れない・終端)「終了する」ボタンは StatusActions.tsx の手動操作。
6.2 課題
- スコア入力完了と大会 done が手動で結ばれていない (押し忘れ発生)
- 「予選が完了して決勝Tが進行可能になった」という中間状態が UI 上に表現されていない
- 標準 (実チーム解決前) と確定 (実チーム解決後) の UI 表示切替の確実な瞬間 が無い
7. 順位確定 / 大会終了周り (#164 + 本拡張の方針)
7.1 「予選確定」(暗黙・自動)
方針: 案A = 自動反映
- 予選グループ単位で全試合
status=doneになった瞬間にgroupComplete=true - API の
resolveBracketは既に対応済 → UI が即時反映 されるよう、AutoRefresh 経由で5秒以内に追従 - 「予選確定ボタン」のような明示操作は 入れない
7.2 「順位確定」(明示操作・#164)
- 全試合 (予選+決勝T) が
doneまたはscoreLockedの状態で「順位を確定する」ボタンを押せる (押すまでは disabled) - 押すと:
- 大会
status=done - 順位スナップショット保存 (TBD)
- 大会
- 戻せない (
done終端)
7.3 「強制終了」(副動線・既存ボタンのリネーム)
- 既存「終了する」ボタンを 「強制終了」 にリネーム + UI セカンダリ寄せ
- 「順位確定」前提条件を満たさない大会 (事故・天候打ち切り等) で使う
- 条件チェックなしで
doneに遷移可能
8. 既知の懸念・TODO
| # | 懸念 | 対応案 |
|---|---|---|
| (a) | スタッフ画面 /staff/[tid] で決勝Tのチーム名が解決されない可能性 | 実機で再現確認 → UI 側の取得経路を loadBracket 経由に修正 |
| (b) | AutoRefresh 5秒 × 複数端末でDB負荷が無視できないレベルになる可能性 | 中長期で WebSocket/SSE 化を検討 (MVP では現状維持) |
| (c) | スコア確定の「スタッフ送信 → 運営ロック」2段階が現場で混乱を生むか | 運用検証必要。MVP は現状維持 |
| (d) | 順位スナップショット保存 (tournament_standings_snapshot) が必要かは未決 | done 後はスコア修正不可なので不要かも (TBD: #164) |
| (e) | グループ予選で「全試合done だが運営ロック未」状態の決勝T進行可否 | 仕様確認: 解決して進めて良い? それともロック必須? |
9. 関連 Issue / 実装
- Issue #164 順位確定操作の新設 / 終了ボタンの強制終了化
- Issue #160 決勝T プレーオフ形式オプション
- 実装:
apps/api/src/timetable/{generate,resolve-bracket,load-bracket,load-standings,standings}.ts - 実装:
apps/api/src/manage/scores.ts(スコア入力 API) - 実装:
apps/web/src/app/(staff)/staff/[tid]/m/[mid]/ScoringClient.tsx(現場スコア入力 UI) - 実装:
apps/web/src/components/AutoRefresh.tsx(ポーリング基盤)