diff --git a/src/handlers/kboHandlers.ts b/src/handlers/kboHandlers.ts index b7f2a86..d787c24 100644 --- a/src/handlers/kboHandlers.ts +++ b/src/handlers/kboHandlers.ts @@ -5,6 +5,7 @@ import { getPlayerStats, getValidPlayerTypes } from "../services/playerService"; import { getGameList } from "../services/gameListService"; import { getGameDetail } from "../services/gameDetailService"; import { TeamCode } from "../types/panit"; +import { kboTodayKst } from "../types/dateString"; enum KboPath { Rank = "rank", @@ -60,9 +61,12 @@ export const kbo = onRequest(async (req, res) => { const year = parseIntParam(req.query.year, now.getFullYear()); const month = parseIntParam(req.query.month, now.getMonth() + 1); const series = req.query.series ? String(req.query.series) : undefined; - const day = req.query.day !== undefined - ? parseIntParam(req.query.day, NaN) - : undefined; + const dayRaw = req.query.day; + const day = dayRaw === undefined ? + undefined : + String(dayRaw) === "today" ? + parseInt(kboTodayKst().slice(8, 10), 10) : + parseIntParam(dayRaw, NaN); if (year < 1982 || year > 2100) { res.status(400).json({ error: `Invalid year: ${year} (1982~2100)` }); diff --git a/src/types/dateString.ts b/src/types/dateString.ts index 7147ab5..ce9ab71 100644 --- a/src/types/dateString.ts +++ b/src/types/dateString.ts @@ -38,6 +38,18 @@ export function todayKst(): DateString { return toDateString(new Date()); } +/** KBO 데이터 기준일의 컷오프 시각(KST). 일일 갱신(02:00) 직후 버퍼 1시간. */ +const KBO_DAY_CUTOFF_HOUR = 3; + +/** + * KBO 데이터 기준의 "오늘". 일일 갱신 잡이 02:00(KST)에 돌기 때문에 + * 새벽 03:00 이전에는 당일 데이터가 아직 확정되지 않아 직전일을 반환한다. + * `now`는 테스트 주입용(기본 현재 시각). + */ +export function kboTodayKst(now: Date = new Date()): DateString { + return toDateString(new Date(now.getTime() - KBO_DAY_CUTOFF_HOUR * 60 * 60 * 1000)); +} + /** n일 전 KST 날짜. */ export function daysAgoKst(n: number): DateString { return toDateString(new Date(Date.now() - n * 24 * 60 * 60 * 1000)); diff --git a/tests/types/dateString.test.ts b/tests/types/dateString.test.ts new file mode 100644 index 0000000..3718263 --- /dev/null +++ b/tests/types/dateString.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { kboTodayKst } from "../../src/types/dateString"; + +// KST 벽시계 시각으로 Date를 만든다(UTC+09:00 명시). 실행 환경 TZ와 무관. +function kst(iso: string): Date { + return new Date(`${iso}+09:00`); +} + +describe("kboTodayKst — 새벽 03:00(KST) 컷오프", () => { + it("02:59이면 직전일 (일일 갱신 02:00 직후 버퍼)", () => { + expect(kboTodayKst(kst("2026-06-30T02:59:00"))).toBe("2026-06-29"); + }); + + it("00:00(자정)이면 직전일", () => { + expect(kboTodayKst(kst("2026-06-30T00:00:00"))).toBe("2026-06-29"); + }); + + it("03:00 정각이면 당일 (컷오프 경계 포함)", () => { + expect(kboTodayKst(kst("2026-06-30T03:00:00"))).toBe("2026-06-30"); + }); + + it("03:01이면 당일", () => { + expect(kboTodayKst(kst("2026-06-30T03:01:00"))).toBe("2026-06-30"); + }); + + it("정오면 당일", () => { + expect(kboTodayKst(kst("2026-06-30T12:00:00"))).toBe("2026-06-30"); + }); + + it("23:59이면 당일", () => { + expect(kboTodayKst(kst("2026-06-30T23:59:00"))).toBe("2026-06-30"); + }); + + it("월초 새벽이면 전월 말일로 넘어간다", () => { + expect(kboTodayKst(kst("2026-07-01T02:00:00"))).toBe("2026-06-30"); + }); + + it("연초 새벽이면 전년 말일로 넘어간다", () => { + expect(kboTodayKst(kst("2026-01-01T01:00:00"))).toBe("2025-12-31"); + }); +});