From a7ce2799efbe49038151e31112d2d14a1cae12b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Tue, 14 Apr 2026 22:29:23 +0900 Subject: [PATCH] Merge live game data into KBO schedule results for today. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KBO 일정 조회 시 오늘 날짜의 경기에 대해 실시간 점수와 경기 상태를 병합하는 기능을 추가했습니다. - `GameStatus` 타입에 'live'를 추가하고, 게임센터 API 데이터를 기반으로 상태를 변환하는 로직을 구현했습니다. - 외부 API 연동 시 발생할 수 있는 일시적인 오류(503 등)에 대비하여 예외 발생 시 기존 캐시 데이터를 반환하도록 예외 처리를 적용했습니다. - 경기 ID 또는 팀 정보와 시간을 조합한 복합 키를 사용하여 스케줄 데이터와 실시간 데이터를 정확하게 매칭합니다. --- src/kbo/schedule.ts | 2 +- src/repositories/kboRepository.ts | 76 ++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) 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 };