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