diff --git a/src/repositories/kboRepository.ts b/src/repositories/kboRepository.ts index bf39145..7c08d35 100644 --- a/src/repositories/kboRepository.ts +++ b/src/repositories/kboRepository.ts @@ -205,6 +205,44 @@ function liveKey( * @param series 시리즈 식별자(정규시즌/포스트시즌 등). `getGameList`에 그대로 전달된다. * @returns 실시간 점수·상태가 반영된 새 배열. 미매칭 항목은 원본 객체를 그대로 포함한다. */ +/** + * 오늘 라이브 경기 목록의 짧은 TTL 인메모리 캐시(키: ymd|series). + * + * `gatherUserContext`가 채팅 매 호출마다 오늘 일정을 조립하면서 `getGameList`를 + * 부르는데(시스템 프롬프트의 {{todaySchedule}}), 동시 사용자가 많으면 외부 KBO + * 엔드포인트로 크롤이 그대로 증폭된다. 같은 시간창의 호출이 1회 fetch를 공유하도록 + * 짧게(기본 30초) 캐시한다 — 스코어 신선도는 사실상 유지하면서 외부 호출을 급감시킨다. + * 인스턴스 로컬이라 콜드스타트/스케일아웃마다 비지만, best-effort 완화로 충분하다. + */ +const LIVE_GAMELIST_TTL_MS = 60_000; +const liveGameListCache = new Map(); + +/** + * KST 경기 시간대만 라이브 merge를 허용하는 게이트. + * + * KBO 경기는 대략 14:00(주말)~23:30 KST에 진행된다. 그 외 시간엔 오늘 경기가 + * "예정" 또는 "종료"로 고정이라 라이브 fetch가 매번 같은 값을 돌려준다. 자정 넘긴 + * 연장 경기는 날짜(ymd)가 어제로 남아 상위의 `ymd !== today` 분기에서 이미 걸러진다. + */ +// 서버 로컬시간 = KST(.env의 TZ=Asia/Seoul). 위의 formatYmd(new Date())와 동일 기준. +const KST_GAME_WINDOW_START_HOUR = 14; + +async function getLiveGameListCached( + ymd: string, + series: string | undefined +): Promise { + const key = `${ymd}|${series ?? ""}`; + const hit = liveGameListCache.get(key); + const now = Date.now(); + if (hit && now - hit.at < LIVE_GAMELIST_TTL_MS) { + console.log(`[kbo-merge] live cache hit key=${key} age=${now - hit.at}ms`); + return hit.games; + } + const result = await getGameList(ymd, series); + liveGameListCache.set(key, { at: now, games: result.games }); + return result.games; +} + async function mergeLiveIntoSchedule( ymd: string, games: ScheduleGame[], @@ -220,11 +258,17 @@ async function mergeLiveIntoSchedule( return games; } + // 경기 시간대 밖이면 라이브 정보가 변하지 않으므로 외부 호출 없이 즉시 반환한다. + const hour = new Date().getHours(); + if (hour < KST_GAME_WINDOW_START_HOUR) { + console.log(`[kbo-merge] skip: off-hours kstHour=${hour}`); + return games; + } + // 실시간 경기 목록 조회. 네트워크/파싱 실패 시 원본 일정으로 graceful degradation. let live: GameListRecord[]; try { - const result = await getGameList(ymd, series); - live = result.games; + live = await getLiveGameListCached(ymd, series); console.log(`[kbo-merge] fetched live count=${live.length}`); } catch (e) { // 실패를 호출자에게 전파하지 않는다 — 일정 화면이 깨지는 것보다 점수 미반영이 낫다.