From c7e4ed8c2288162eb4fbd6b6deaa3cbd519fab05 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 23:08:45 +0900 Subject: [PATCH] Standardize game status values and make favorite team optional. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `GameStatus` 타입을 `scheduled`, `live`, `completed`, `cancelled`로 변경하여 상태 관리의 일관성을 높였습니다. - 사용자 프로필의 `favoriteTeamCode`를 선택 사항으로 변경하고, null 또는 undefined 입력에 대한 처리를 추가했습니다. - KBO 데이터 병합 과정에 상세 로그를 도입하여 실시간 데이터 매칭 및 외부 API 호출 실패 상황에 대한 가시성을 확보했습니다. - 상태 값 변경에 따라 영향받는 서비스 로직과 테스트 코드를 일괄 업데이트했습니다. --- .env | 1 + .idea/AICommit.xml | 2 +- .idea/runConfigurations/Debug_Functions.xml | 5 + TODO.md | 29 +++++ database-debug.log | 13 -- firestore-debug.log | 127 -------------------- src/repositories/kboRepository.ts | 23 +++- src/repositories/userRepository.ts | 4 +- src/services/gameResultService.ts | 4 +- src/services/gameSyncService.ts | 10 +- src/services/predictionService.ts | 2 +- src/services/userService.ts | 13 +- src/types/panit.ts | 4 +- tests/repositories/gameRepository.test.ts | 6 +- tests/services/gameSyncService.test.ts | 12 +- 15 files changed, 80 insertions(+), 175 deletions(-) create mode 100644 .env create mode 100644 .idea/runConfigurations/Debug_Functions.xml create mode 100644 TODO.md delete mode 100644 database-debug.log delete mode 100644 firestore-debug.log diff --git a/.env b/.env new file mode 100644 index 0000000..02fdee7 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +TZ=Asia/Seoul diff --git a/.idea/AICommit.xml b/.idea/AICommit.xml index 7c2bd18..6509062 100644 --- a/.idea/AICommit.xml +++ b/.idea/AICommit.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/runConfigurations/Debug_Functions.xml b/.idea/runConfigurations/Debug_Functions.xml new file mode 100644 index 0000000..b30d01c --- /dev/null +++ b/.idea/runConfigurations/Debug_Functions.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..d868440 --- /dev/null +++ b/TODO.md @@ -0,0 +1,29 @@ +# TODO + +## KBO 일정/라이브 머지 관련 + +### 남은 작업 +- [ ] **클라이언트 쪽 점수 표시 이슈 조사** + - 서버 머지는 정상 (로그 확인 완료: 5/5 매칭, 점수·상태 정상 반영) + - 브라우저 Network 탭에서 실제 응답 바디에 `awayScore`/`homeScore` 값이 들어오는지 확인 + - 값이 들어오는데 0으로 보이면 → 프런트 렌더링 로직 점검 + - 값이 null/0으로 오면 → 응답 직렬화/DTO 매핑 레이어 점검 (`scheduleService.ts` 등) + +- [ ] **`parseGameId` 수정** (`src/kbo/schedule.ts:151`) + - 현재 Schedule.aspx에서 모든 게임의 `gameId`가 null로 파싱됨 (로그에서 `gameId=null` 5건 확인) + - 정규식 `/gameId=([A-Za-z0-9]+)/`가 실제 relay 셀 HTML 구조와 안 맞을 가능성 + - 지금은 `(awayTeamCode, homeTeamCode, time)` 폴백으로 매칭되고 있지만 **더블헤더 발생 시 같은 카드 2경기가 섞일 위험** + - KBO 응답의 relay 셀 실제 HTML을 확인하고 파서 보정 필요 + +- [ ] **`mergeLiveIntoSchedule` 진단 로그 제거** (`src/repositories/kboRepository.ts`) + - 점수 이슈 해결 후 `[kbo-merge]` console.log 전부 제거 + +- [ ] **deploy 스크립트에 build 포함** + - `package.json:10` `"deploy": "firebase deploy --only functions"` → `"tsc && firebase deploy --only functions"` + - 현재는 `npm run build` 빠뜨리면 구버전 `lib/` 그대로 배포됨 + +### 향후 확장 여지 +- [ ] 라이브 상세 필드 별도 엔드포인트로 노출 + - 이닝/카운트/주자/현재 타자·투수는 "스케줄 파악" 목적에서 제외했으나, 라이브 상세 화면 필요 시 `getGameList` 직접 노출하는 별도 핸들러 추가 고려 +- [ ] `formatYmd(new Date())` → `todayKst()` 기반으로 통일 + - 현재 TZ=Asia/Seoul 의존. 환경 변경 시 취약 \ No newline at end of file diff --git a/database-debug.log b/database-debug.log deleted file mode 100644 index 87ab9c6..0000000 --- a/database-debug.log +++ /dev/null @@ -1,13 +0,0 @@ -WARNING: A terminally deprecated method in sun.misc.Unsafe has been called -WARNING: sun.misc.Unsafe::objectFieldOffset has been called by akka.util.Unsafe (file:/C:/Users/%ec%9c%a4%ec%a0%95%eb%af%bc/.cache/firebase/emulators/firebase-database-emulator-v4.11.2.jar) -WARNING: Please consider reporting this to the maintainers of class akka.util.Unsafe -WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release -12:30:05.819 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started -12:30:05.923 [main] INFO com.firebase.server.forge.App$ - Listening at 0.0.0.0:9000 -12:30:09.083 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.NamespaceActor - mmday-panit-default-rtdb successfully activated FBKV (SurveyIdle(0)) wait: 80ms, init: 0ms -12:30:09.125 [NamespaceSystem-blocking-namespace-operation-dispatcher-6] INFO com.firebase.core.namespace.StateManager - Namespace mmday-panit-default-rtdb status Active to Active -12:30:13.051 [FirebaseWorkerPool-1-3] WARN com.firebase.core.persistence.backend.dummy.EmulatorMetadataPersistence - Multiple projectIds are not recommended in single project mode. Requested -namespace mmday-panit, but the emulator is configured for mmday-panit-default-rtdb. To -opt-out of single project mode add/set the singleProjectMode: false property in -the firebase.json emulators config. -12:30:13.054 [NamespaceSystem-akka.actor.default-dispatcher-5] INFO com.firebase.core.namespace.NamespaceActor - mmday-panit successfully activated FBKV (SurveyIdle(0)) wait: 1ms, init: 0ms diff --git a/firestore-debug.log b/firestore-debug.log deleted file mode 100644 index 2824cdb..0000000 --- a/firestore-debug.log +++ /dev/null @@ -1,127 +0,0 @@ -WARNING: A terminally deprecated method in sun.misc.Unsafe has been called -WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/C:/Users/%ec%9c%a4%ec%a0%95%eb%af%bc/.cache/firebase/emulators/cloud-firestore-emulator-v1.20.4.jar) -WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2 -WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release -Apr 13, 2026 12:30:04 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketServer start -INFO: Started WebSocket server on ws://0.0.0.0:9150 - -API endpoint: http://0.0.0.0:8080 -Database Edition: STANDARD -Database Mode: CLOUD_FIRESTORE - -If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run: - - export FIRESTORE_EMULATOR_HOST=0.0.0.0:8080 - -If you are running a Firestore in Datastore Mode project, run: - - export DATASTORE_EMULATOR_HOST=0.0.0.0:8080 - -Note: Support for Datastore Mode is in preview. If you encounter any bugs please file at https://github.com/firebase/firebase-tools/issues. -Dev App Server is now running. - -Apr 13, 2026 12:30:10 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected HTTP/2 connection. -Apr 13, 2026 12:30:11 PM io.netty.channel.DefaultChannelPipeline onUnhandledInboundException -WARNING: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception. -java.net.SocketException: Connection reset - at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:398) - at java.base/sun.nio.ch.SocketChannelImpl.implRead(SocketChannelImpl.java:430) - at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:488) - at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:255) - at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) - at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:356) - at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) - at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:796) - at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:732) - at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:658) - at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) - at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998) - at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) - at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) - at java.base/java.lang.Thread.run(Thread.java:1516) - -Apr 13, 2026 12:30:11 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected HTTP/2 connection. -Apr 13, 2026 12:30:12 PM io.netty.channel.DefaultChannelPipeline onUnhandledInboundException -WARNING: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception. -java.net.SocketException: Connection reset - at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:398) - at java.base/sun.nio.ch.SocketChannelImpl.implRead(SocketChannelImpl.java:430) - at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:488) - at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:255) - at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) - at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:356) - at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) - at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:796) - at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:732) - at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:658) - at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) - at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998) - at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) - at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) - at java.base/java.lang.Thread.run(Thread.java:1516) - -Apr 13, 2026 12:30:13 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected HTTP/2 connection. -Apr 13, 2026 12:30:14 PM io.netty.channel.DefaultChannelPipeline onUnhandledInboundException -WARNING: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception. -java.net.SocketException: Connection reset - at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:398) - at java.base/sun.nio.ch.SocketChannelImpl.implRead(SocketChannelImpl.java:430) - at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:488) - at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:255) - at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) - at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:356) - at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) - at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:796) - at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:732) - at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:658) - at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) - at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998) - at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) - at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) - at java.base/java.lang.Thread.run(Thread.java:1516) - -Apr 13, 2026 12:30:14 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected HTTP/2 connection. -Apr 13, 2026 12:30:14 PM io.netty.channel.DefaultChannelPipeline onUnhandledInboundException -WARNING: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception. -java.net.SocketException: Connection reset - at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:398) - at java.base/sun.nio.ch.SocketChannelImpl.implRead(SocketChannelImpl.java:430) - at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:488) - at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:255) - at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) - at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:356) - at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) - at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:796) - at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:732) - at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:658) - at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) - at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998) - at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) - at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) - at java.base/java.lang.Thread.run(Thread.java:1516) - -Apr 13, 2026 12:30:15 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead -INFO: Detected HTTP/2 connection. -Apr 13, 2026 12:30:15 PM io.netty.channel.DefaultChannelPipeline onUnhandledInboundException -WARNING: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception. -java.net.SocketException: Connection reset - at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:398) - at java.base/sun.nio.ch.SocketChannelImpl.implRead(SocketChannelImpl.java:430) - at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:488) - at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:255) - at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) - at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:356) - at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) - at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:796) - at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:732) - at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:658) - at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) - at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998) - at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) - at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) - at java.base/java.lang.Thread.run(Thread.java:1516) - diff --git a/src/repositories/kboRepository.ts b/src/repositories/kboRepository.ts index 29edcdc..6a53bc8 100644 --- a/src/repositories/kboRepository.ts +++ b/src/repositories/kboRepository.ts @@ -193,13 +193,19 @@ async function mergeLiveIntoSchedule( series: string | undefined ): Promise { const today = formatYmd(new Date()); - if (ymd !== today) return games; + console.log(`[kbo-merge] enter ymd=${ymd} today=${today} games=${games.length}`); + if (ymd !== today) { + console.log(`[kbo-merge] skip: not today`); + return games; + } let live: GameListRecord[]; try { const result = await getGameList(ymd, series); live = result.games; - } catch { + console.log(`[kbo-merge] fetched live count=${live.length}`); + } catch (e) { + console.log(`[kbo-merge] fetch failed`, e); return games; } @@ -213,19 +219,26 @@ async function mergeLiveIntoSchedule( ); } - return games.map((g) => { + let matched = 0; + const result = games.map((g) => { const rec = (g.gameId && byId.get(g.gameId)) || byComposite.get(liveKey(g.awayTeamCode, g.homeTeamCode, g.time)); - if (!rec) return g; - + if (!rec) { + console.log(`[kbo-merge] miss gameId=${g.gameId} key=${liveKey(g.awayTeamCode, g.homeTeamCode, g.time)}`); + return g; + } + matched++; 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; + console.log(`[kbo-merge] hit gameId=${g.gameId} state=${rec.status.stateCode} score=${merged.awayScore}:${merged.homeScore} status=${merged.status}`); return merged; }); + console.log(`[kbo-merge] done matched=${matched}/${games.length}`); + return result; } async function readDayDocs(keys: string[]): Promise> { diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 27f81c6..8ca2066 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -14,7 +14,7 @@ export interface RegisterInput { email: string; photoUrl?: string; provider: Provider; - favoriteTeamCode: TeamCode; + favoriteTeamCode?: TeamCode; knowledgeLevel: KnowledgeLevel; } @@ -53,11 +53,11 @@ export async function createUser(uid: string, input: RegisterInput): Promise { const game = await getGame(gameId); if (!game) throw new HttpError(404, `game not found: ${gameId}`); - if (game.status !== "waiting") { + if (game.status !== "scheduled") { throw new HttpError(409, `game status is ${game.status}, voting closed`); } return game; diff --git a/src/services/userService.ts b/src/services/userService.ts index c05869e..8c5855b 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -105,11 +105,14 @@ export async function createMe( } const displayName = parseDisplayName(body.displayName); - const favoriteTeamCode = parseEnum( - "favoriteTeamCode", - body.favoriteTeamCode, - TEAM_CODE_VALUES - ); + const favoriteTeamCode = + body.favoriteTeamCode === undefined || body.favoriteTeamCode === null + ? undefined + : parseEnum( + "favoriteTeamCode", + body.favoriteTeamCode, + TEAM_CODE_VALUES + ); const knowledgeLevel = parseEnum( "knowledgeLevel", body.knowledgeLevel, diff --git a/src/types/panit.ts b/src/types/panit.ts index 6c80b2b..dd97db5 100644 --- a/src/types/panit.ts +++ b/src/types/panit.ts @@ -1,7 +1,7 @@ import type { Timestamp } from "firebase-admin/firestore"; export type Provider = "google" | "apple"; -export type GameStatus = "waiting" | "live" | "ended" | "canceled"; +export type GameStatus = "scheduled" | "live" | "completed" | "cancelled"; export enum TeamCode { KT = "KT", @@ -27,7 +27,7 @@ export interface User { email: string; photoUrl?: string; provider: Provider; - favoriteTeamCode: TeamCode; + favoriteTeamCode?: TeamCode; knowledgeLevel: KnowledgeLevel; createdAt: Timestamp; } diff --git a/tests/repositories/gameRepository.test.ts b/tests/repositories/gameRepository.test.ts index 0a6cc10..37a3eec 100644 --- a/tests/repositories/gameRepository.test.ts +++ b/tests/repositories/gameRepository.test.ts @@ -20,7 +20,7 @@ describe("gameRepository", () => { const game1 = { time: Timestamp.fromDate(new Date("2026-04-12T09:30:00.000Z")), stadium: "잠실", - status: "waiting" as const, + status: "scheduled" as const, homeTeamCode: "LG", awayTeamCode: "HT", }; @@ -28,7 +28,7 @@ describe("gameRepository", () => { const game2 = { time: Timestamp.fromDate(new Date("2026-04-12T05:00:00.000Z")), stadium: "문학", - status: "waiting" as const, + status: "scheduled" as const, homeTeamCode: "SK", awayTeamCode: "WO", }; @@ -36,7 +36,7 @@ describe("gameRepository", () => { const game3 = { time: Timestamp.fromDate(new Date("2026-04-11T09:30:00.000Z")), stadium: "사직", - status: "ended" as const, + status: "completed" as const, homeTeamCode: "LT", awayTeamCode: "KT", }; diff --git a/tests/services/gameSyncService.test.ts b/tests/services/gameSyncService.test.ts index 389c4f8..9f0a27e 100644 --- a/tests/services/gameSyncService.test.ts +++ b/tests/services/gameSyncService.test.ts @@ -39,26 +39,26 @@ describe("gameSyncService.toTime", () => { }); describe("gameSyncService.toGameDoc", () => { - it("scheduled 경기는 status=waiting, 승리팀 없음", () => { + it("scheduled 경기는 status=scheduled, 승리팀 없음", () => { const doc = toGameDoc(2026, baseGame()); expect(doc).not.toBeNull(); - expect(doc!.status).toBe("waiting"); + expect(doc!.status).toBe("scheduled"); expect(doc!.homeTeamCode).toBe("LG"); expect(doc!.winningTeamCode).toBeUndefined(); }); - it("completed 경기는 status=ended, 점수 기반 winningTeamCode 설정", () => { + it("completed 경기는 status=completed, 점수 기반 winningTeamCode 설정", () => { const doc = toGameDoc( 2026, baseGame({ status: "completed", homeScore: 7, awayScore: 2 }) ); - expect(doc!.status).toBe("ended"); + expect(doc!.status).toBe("completed"); expect(doc!.winningTeamCode).toBe("LG"); }); - it("cancelled 경기는 status=canceled", () => { + it("cancelled 경기는 status=cancelled", () => { const doc = toGameDoc(2026, baseGame({ status: "cancelled" })); - expect(doc!.status).toBe("canceled"); + expect(doc!.status).toBe("cancelled"); }); it("무승부는 winningTeamCode가 없다", () => {