Merge live game data into KBO schedule results for today.
- KBO 일정 조회 시 오늘 날짜의 경기에 대해 실시간 점수와 경기 상태를 병합하는 기능을 추가했습니다. - `GameStatus` 타입에 'live'를 추가하고, 게임센터 API 데이터를 기반으로 상태를 변환하는 로직을 구현했습니다. - 외부 API 연동 시 발생할 수 있는 일시적인 오류(503 등)에 대비하여 예외 발생 시 기존 캐시 데이터를 반환하도록 예외 처리를 적용했습니다. - 경기 ID 또는 팀 정보와 시간을 조합한 복합 키를 사용하여 스케줄 데이터와 실시간 데이터를 정확하게 매칭합니다.
This commit is contained in:
parent
98502916de
commit
a7ce2799ef
@ -14,7 +14,7 @@ const USER_AGENT =
|
|||||||
|
|
||||||
// ── Types ──
|
// ── Types ──
|
||||||
|
|
||||||
export type GameStatus = "completed" | "cancelled" | "scheduled";
|
export type GameStatus = "completed" | "cancelled" | "scheduled" | "live";
|
||||||
|
|
||||||
export interface ScheduleGame {
|
export interface ScheduleGame {
|
||||||
date: string;
|
date: string;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
type ScheduleFilters,
|
type ScheduleFilters,
|
||||||
type ScheduleResult,
|
type ScheduleResult,
|
||||||
type ScheduleGame,
|
type ScheduleGame,
|
||||||
|
type GameStatus,
|
||||||
} from "../kbo/schedule.js";
|
} from "../kbo/schedule.js";
|
||||||
import {
|
import {
|
||||||
fetchPlayerStatsInitial,
|
fetchPlayerStatsInitial,
|
||||||
@ -25,6 +26,8 @@ import {
|
|||||||
releaseLock,
|
releaseLock,
|
||||||
} from "./kboCacheRepository.js";
|
} from "./kboCacheRepository.js";
|
||||||
import { firestore } from "../firebase.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
|
const TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
@ -164,6 +167,67 @@ function dayTtlMs(yyyymmdd: string, games: ScheduleGame[]): number {
|
|||||||
return THIRTY_SEC_MS;
|
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<ScheduleGame[]> {
|
||||||
|
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<string, GameListRecord>();
|
||||||
|
const byComposite = new Map<string, GameListRecord>();
|
||||||
|
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<Map<string, ScheduleGame[] | null>> {
|
async function readDayDocs(keys: string[]): Promise<Map<string, ScheduleGame[] | null>> {
|
||||||
if (keys.length === 0) return new Map();
|
if (keys.length === 0) return new Map();
|
||||||
const refs = keys.map((k) => firestore.collection(CACHE_COLLECTION).doc(k));
|
const refs = keys.map((k) => firestore.collection(CACHE_COLLECTION).doc(k));
|
||||||
@ -224,11 +288,13 @@ async function fetchScheduleSingleDay(
|
|||||||
cached = (await readDayDocs([key])).get(key) ?? [];
|
cached = (await readDayDocs([key])).get(key) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const merged = await mergeLiveIntoSchedule(ymd, cached, filters.series);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
year: filters.year,
|
year: filters.year,
|
||||||
month: filters.month,
|
month: filters.month,
|
||||||
day: filters.day,
|
day: filters.day,
|
||||||
games: cached,
|
games: merged,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,9 +359,15 @@ async function fetchScheduleMonth(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const games: ScheduleGame[] = [];
|
const games: ScheduleGame[] = [];
|
||||||
|
const today = formatYmd(new Date());
|
||||||
for (const ymd of allDays) {
|
for (const ymd of allDays) {
|
||||||
const list = cached.get(dayKey(ymd, filters.team, filters.series)) ?? [];
|
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 };
|
return { year: filters.year, month: filters.month, games };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user