diff --git a/src/kbo/schedule.ts b/src/kbo/schedule.ts index 673140a..54c8b37 100644 --- a/src/kbo/schedule.ts +++ b/src/kbo/schedule.ts @@ -14,7 +14,7 @@ const USER_AGENT = // ── Types ── -export type GameStatus = "completed" | "cancelled" | "scheduled"; +export type GameStatus = "completed" | "cancelled" | "scheduled" | "live"; export interface ScheduleGame { date: string; diff --git a/src/repositories/kboRepository.ts b/src/repositories/kboRepository.ts index 9556cc6..29edcdc 100644 --- a/src/repositories/kboRepository.ts +++ b/src/repositories/kboRepository.ts @@ -8,6 +8,7 @@ import { type ScheduleFilters, type ScheduleResult, type ScheduleGame, + type GameStatus, } from "../kbo/schedule.js"; import { fetchPlayerStatsInitial, @@ -25,6 +26,8 @@ import { releaseLock, } from "./kboCacheRepository.js"; import { firestore } from "../firebase.js"; +import { getGameList } from "../services/gameListService.js"; +import type { GameListRecord } from "../kbo/game-list.js"; const TTL_MS = 60 * 60 * 1000; // 1 hour @@ -164,6 +167,67 @@ function dayTtlMs(yyyymmdd: string, games: ScheduleGame[]): number { return THIRTY_SEC_MS; } +function statusFromRecord(rec: GameListRecord): GameStatus | null { + // cancelCode: "0" = 정상경기, 그 외 = 취소/노게임 + if (rec.status.cancelCode && rec.status.cancelCode !== "0") return "cancelled"; + // stateCode: "1" = 예정, "2" = 진행 중, "3" = 종료 + switch (rec.status.stateCode) { + case "1": return "scheduled"; + case "2": return "live"; + case "3": return "completed"; + default: return null; + } +} + +function liveKey( + away: string, + home: string, + time: string +): string { + return `${away}|${home}|${time}`; +} + +async function mergeLiveIntoSchedule( + ymd: string, + games: ScheduleGame[], + series: string | undefined +): Promise { + const today = formatYmd(new Date()); + if (ymd !== today) return games; + + let live: GameListRecord[]; + try { + const result = await getGameList(ymd, series); + live = result.games; + } catch { + return games; + } + + const byId = new Map(); + const byComposite = new Map(); + for (const rec of live) { + if (rec.gameId) byId.set(rec.gameId, rec); + byComposite.set( + liveKey(rec.awayTeamCode, rec.homeTeamCode, rec.time), + rec + ); + } + + return games.map((g) => { + const rec = + (g.gameId && byId.get(g.gameId)) || + byComposite.get(liveKey(g.awayTeamCode, g.homeTeamCode, g.time)); + if (!rec) return g; + + const merged: ScheduleGame = { ...g }; + if (rec.score.away != null) merged.awayScore = rec.score.away; + if (rec.score.home != null) merged.homeScore = rec.score.home; + const nextStatus = statusFromRecord(rec); + if (nextStatus) merged.status = nextStatus; + return merged; + }); +} + async function readDayDocs(keys: string[]): Promise> { if (keys.length === 0) return new Map(); const refs = keys.map((k) => firestore.collection(CACHE_COLLECTION).doc(k)); @@ -224,11 +288,13 @@ async function fetchScheduleSingleDay( cached = (await readDayDocs([key])).get(key) ?? []; } + const merged = await mergeLiveIntoSchedule(ymd, cached, filters.series); + return { year: filters.year, month: filters.month, day: filters.day, - games: cached, + games: merged, }; } @@ -293,9 +359,15 @@ async function fetchScheduleMonth( } const games: ScheduleGame[] = []; + const today = formatYmd(new Date()); for (const ymd of allDays) { const list = cached.get(dayKey(ymd, filters.team, filters.series)) ?? []; - games.push(...list); + if (ymd === today) { + const merged = await mergeLiveIntoSchedule(ymd, list, filters.series); + games.push(...merged); + } else { + games.push(...list); + } } return { year: filters.year, month: filters.month, games };