o2o-triple-pick/lib/schedule.ts

136 lines
4.5 KiB
TypeScript

// 2026 FIFA World Cup Group A 정식 일정 (출처: Wikipedia/FIFA, KST 변환 검증 2026-06-08)
// 모든 kickoffKst는 한국시간(+09:00). lockAt = 킥오프 -10분.
import type { Match, Team } from "./types";
const KOR: Team = { name: "Korea Republic", shortName: "한국", code: "KOR", flag: "🇰🇷" };
const CZE: Team = { name: "Czechia", shortName: "체코", code: "CZE", flag: "🇨🇿" };
const MEX: Team = { name: "Mexico", shortName: "멕시코", code: "MEX", flag: "🇲🇽" };
const RSA: Team = { name: "South Africa", shortName: "남아공", code: "RSA", flag: "🇿🇦" };
// 투표 오픈 = 킥오프 -48시간 (D-2). 마감 = 킥오프 정각(경기 시작 전까지).
function opens(iso: string): string {
return new Date(new Date(iso).getTime() - 48 * 3600000).toISOString();
}
// Group A 전체 6경기 — 경기별 정확 일시 자동 반영용 SSOT
export const GROUP_A: Match[] = [
{
matchId: "A_MEX_RSA_20260612",
roundLabel: "Match 01 · 개막전",
group: "A",
teamA: MEX,
teamB: RSA,
kickoffKst: "2026-06-12T04:00:00+09:00",
venue: "Estadio Azteca · Mexico City",
opensAt: opens("2026-06-12T04:00:00+09:00"),
lockAt: "2026-06-12T04:00:00+09:00",
status: "upcoming",
hookText: "글로벌 축구 개막전, AI는 개최국을 믿을까",
result: null,
},
{
matchId: "A_KOR_CZE_20260612",
roundLabel: "Match 01",
group: "A",
teamA: KOR,
teamB: CZE,
kickoffKst: "2026-06-12T11:00:00+09:00",
venue: "Estadio Guadalajara (Akron)",
opensAt: opens("2026-06-12T11:00:00+09:00"),
lockAt: "2026-06-12T11:00:00+09:00",
status: "upcoming",
hookText: "한국 첫 경기, AI의 선택은 갈렸다",
result: null,
},
{
matchId: "A_CZE_RSA_20260619",
roundLabel: "Match 02",
group: "A",
teamA: CZE,
teamB: RSA,
kickoffKst: "2026-06-19T01:00:00+09:00",
venue: "USA (TBD)",
opensAt: opens("2026-06-19T01:00:00+09:00"),
lockAt: "2026-06-19T01:00:00+09:00",
status: "upcoming",
hookText: "체코 vs 남아공, AI의 예측은",
result: null,
},
{
matchId: "A_MEX_KOR_20260619",
roundLabel: "Match 02",
group: "A",
teamA: MEX,
teamB: KOR,
kickoffKst: "2026-06-19T10:00:00+09:00",
venue: "Estadio Guadalajara (Akron)",
opensAt: opens("2026-06-19T10:00:00+09:00"),
lockAt: "2026-06-19T10:00:00+09:00",
status: "upcoming",
hookText: "개최국 멕시코 vs 한국, AI는 누구 편",
result: null,
},
{
matchId: "A_CZE_MEX_20260625",
roundLabel: "Match 03",
group: "A",
teamA: CZE,
teamB: MEX,
kickoffKst: "2026-06-25T10:00:00+09:00",
venue: "Estadio Azteca · Mexico City",
opensAt: opens("2026-06-25T10:00:00+09:00"),
lockAt: "2026-06-25T10:00:00+09:00",
status: "upcoming",
hookText: "체코 vs 멕시코, 운명의 최종전",
result: null,
},
{
matchId: "A_RSA_KOR_20260625",
roundLabel: "Match 03",
group: "A",
teamA: RSA,
teamB: KOR,
kickoffKst: "2026-06-25T10:00:00+09:00",
venue: "Estadio BBVA · Monterrey",
opensAt: opens("2026-06-25T10:00:00+09:00"),
lockAt: "2026-06-25T10:00:00+09:00",
status: "upcoming",
hookText: "한국 16강의 갈림길, AI의 마지막 예측",
result: null,
},
];
// 현재 노출할 대표 경기 (체코전)
export const FEATURED_MATCH_ID = "A_KOR_CZE_20260612";
export function getMatch(id: string): Match {
return GROUP_A.find((m) => m.matchId === id) ?? GROUP_A[1];
}
export const FEATURED = getMatch(FEATURED_MATCH_ID);
// 투표창 단계(D7): 오픈 전 / 투표중 / 마감(킥오프) / 종료(결과). DEMO_FORCE_OPEN는 UI 측에서 별도 처리.
export type MatchPhase = "scheduled" | "open" | "locked" | "finished";
export function matchPhase(m: Match, now: number = Date.now()): MatchPhase {
if (m.result) return "finished";
if (now >= new Date(m.lockAt).getTime()) return "locked";
if (now < new Date(m.opensAt).getTime()) return "scheduled";
return "open";
}
// 대시보드: 날짜별로 묶고 킥오프 오름차순 정렬
export function matchesByDate(): { date: string; matches: Match[] }[] {
const sorted = [...GROUP_A].sort(
(a, b) => new Date(a.kickoffKst).getTime() - new Date(b.kickoffKst).getTime(),
);
const groups: { date: string; matches: Match[] }[] = [];
for (const m of sorted) {
const key = m.kickoffKst.slice(0, 10);
const last = groups[groups.length - 1];
if (last && last.date === key) last.matches.push(m);
else groups.push({ date: key, matches: [m] });
}
return groups;
}