Skip to content

当日運営フロー仕様 (スコア・順位確定)

📌 目的: 当日運営のうち スコア入力 / 順位算出 / 順位確定 周りの現状実装を整理し、 ポーリング・キャッシュ更新の挙動を明文化する。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/timetableGET /public/timetable/:id✅ resolveBracket 通過解決済表示
運営: /manage/.../prep/TimetableMatchesPanelGET /manage/timetable/:tid/...✅ resolveBracket 通過解決済表示
運営: /manage/.../tournaments/:id/standings (順位タブ)GET /manage/timetable/:tid/standingsloadStandings のみ順位表
スタッフ: /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 (ポーリング基盤)