import { onSchedule } from "firebase-functions/scheduler"; import { logger } from "firebase-functions"; import { firestore } from "../firebase"; import { fetchRankFromKbo, fetchScheduleFromKbo, } from "../repositories/kboRepository"; import { syncGamesForMonth, forceSyncDay, } from "../services/gameSyncService"; import { daysAgoKst } from "../types/dateString"; const CACHE_COLLECTION = "kboCache"; async function invalidateByPrefix(prefix: string): Promise { const snap = await firestore .collection(CACHE_COLLECTION) .where("__name__", ">=", prefix) .where("__name__", "<", prefix + "\uf8ff") .get(); const batch = firestore.batch(); snap.docs.forEach((d) => batch.delete(d.ref)); if (!snap.empty) await batch.commit(); } export const kboDailyRefresh = onSchedule( { schedule: "0 2 * * *", timeZone: "Asia/Seoul", region: "asia-northeast3" }, async () => { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; logger.info(`KBO refresh start: ${year}-${month}`); await invalidateByPrefix("rank__"); await invalidateByPrefix("schedule_day__"); // game_detail__는 응답 기반 동적 TTL(종료 경기 7d, 라이브 30s 등)을 이미 갖고 있어 // 02:00 일괄 무효화 대상에서 제외한다. 일괄 삭제 시 직후 상세 조회가 한꺼번에 미스나 // 외부 재조회 폭주를 유발하므로, TTL 자연 만료에 위임한다. try { await fetchRankFromKbo([year]); await fetchScheduleFromKbo({ year, month }); const thisMonthCount = await syncGamesForMonth(year, month); logger.info(`synced ${thisMonthCount} games for ${year}-${month}`); // 월말 3일 이내면 다음 달도 함께 sync const lastDay = new Date(year, month, 0).getDate(); if (now.getDate() >= lastDay - 2) { const next = new Date(year, month, 1); const ny = next.getFullYear(); const nm = next.getMonth() + 1; const nextCount = await syncGamesForMonth(ny, nm); logger.info(`synced ${nextCount} games for ${ny}-${nm}`); } // 어제 경기 status 확정: 월간 schedule refresh가 월 마지막 날을 놓치는 케이스를 // 라이브 데이터(`getGameList`)로 보완. 휴장일이면 0건 반환되어 무해. try { const yesterdayYmd = daysAgoKst(1).replace(/-/g, ""); const { updated } = await forceSyncDay(yesterdayYmd); logger.info(`forceSyncDay: ${updated} games updated for ${yesterdayYmd}`); } catch (err) { logger.error("forceSyncDay failed", err); } logger.info("KBO refresh complete"); } catch (err) { logger.error("KBO refresh failed", err); } } );