Standardize game status values and make favorite team optional.
- `GameStatus` 타입을 `scheduled`, `live`, `completed`, `cancelled`로 변경하여 상태 관리의 일관성을 높였습니다. - 사용자 프로필의 `favoriteTeamCode`를 선택 사항으로 변경하고, null 또는 undefined 입력에 대한 처리를 추가했습니다. - KBO 데이터 병합 과정에 상세 로그를 도입하여 실시간 데이터 매칭 및 외부 API 호출 실패 상황에 대한 가시성을 확보했습니다. - 상태 값 변경에 따라 영향받는 서비스 로직과 테스트 코드를 일괄 업데이트했습니다.
This commit is contained in:
parent
a7ce2799ef
commit
c7e4ed8c22
2
.idea/AICommit.xml
generated
2
.idea/AICommit.xml
generated
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="com.github.blarc.ai.commits.intellij.plugin.settings.ProjectSettings">
|
||||
<option name="splitButtonActionSelectedLLMClientId" value="2f900cba-1f90-431b-acb2-e4e6ac66b31e" />
|
||||
<option name="splitButtonActionSelectedLLMClientId" value="f7c8a29b-b84b-4803-8efc-e5021c34bace" />
|
||||
</component>
|
||||
</project>
|
||||
5
.idea/runConfigurations/Debug_Functions.xml
generated
Normal file
5
.idea/runConfigurations/Debug_Functions.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Debug Functions" type="ChromiumRemoteDebugType" factoryName="Chromium Remote" port="9229">
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
29
TODO.md
Normal file
29
TODO.md
Normal file
@ -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 의존. 환경 변경 시 취약
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
@ -193,13 +193,19 @@ async function mergeLiveIntoSchedule(
|
||||
series: string | undefined
|
||||
): Promise<ScheduleGame[]> {
|
||||
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<Map<string, ScheduleGame[] | null>> {
|
||||
|
||||
@ -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<voi
|
||||
displayName: input.displayName,
|
||||
email: input.email,
|
||||
provider: input.provider,
|
||||
favoriteTeamCode: input.favoriteTeamCode,
|
||||
knowledgeLevel: input.knowledgeLevel,
|
||||
createdAt: FieldValue.serverTimestamp(),
|
||||
};
|
||||
if (input.photoUrl) doc.photoUrl = input.photoUrl;
|
||||
if (input.favoriteTeamCode) doc.favoriteTeamCode = input.favoriteTeamCode;
|
||||
await firestore.collection(COLLECTION).doc(uid).set(doc, { merge: false });
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ export async function processGameEnd(gameId: string): Promise<{ processed: numbe
|
||||
const game = await getGame(gameId);
|
||||
if (!game) throw new HttpError(404, `game not found: ${gameId}`);
|
||||
if (!game.winningTeamCode) throw new HttpError(400, "winningTeamCode not set");
|
||||
if (game.status !== "ended") throw new HttpError(409, `game status must be ended, got ${game.status}`);
|
||||
if (game.status !== "completed") throw new HttpError(409, `game status must be completed, got ${game.status}`);
|
||||
|
||||
const date = fromTimestamp(game.time);
|
||||
const votes = await getAllUserVotes(gameId);
|
||||
@ -47,7 +47,7 @@ export async function markGameEnded(
|
||||
const snap = await ref.get();
|
||||
if (!snap.exists) throw new HttpError(404, `game not found: ${gameId}`);
|
||||
await ref.update({
|
||||
status: "ended",
|
||||
status: "completed",
|
||||
winningTeamCode,
|
||||
endedAt: FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
@ -2,16 +2,10 @@ import { Timestamp } from "firebase-admin/firestore";
|
||||
import { firestore } from "../firebase.js";
|
||||
import { fetchScheduleFromKbo } from "../repositories/kboRepository.js";
|
||||
import type { ScheduleGame } from "../kbo/schedule.js";
|
||||
import type { Game, GameStatus } from "../types/panit.js";
|
||||
import type { Game } from "../types/panit.js";
|
||||
|
||||
const COLLECTION = "games";
|
||||
|
||||
function toGameStatus(s: ScheduleGame["status"]): GameStatus {
|
||||
if (s === "completed") return "ended";
|
||||
if (s === "cancelled") return "canceled";
|
||||
return "waiting";
|
||||
}
|
||||
|
||||
/**
|
||||
* KBO schedule의 `date`("MM.DD")와 `time`("HH:mm")을 KST 기준 Firestore Timestamp로 변환한다.
|
||||
*/
|
||||
@ -34,7 +28,7 @@ export function toGameDoc(year: number, g: ScheduleGame): Game | null {
|
||||
const doc: Game = {
|
||||
time: toTime(year, g.date, g.time),
|
||||
stadium: g.stadium,
|
||||
status: toGameStatus(g.status),
|
||||
status: g.status,
|
||||
homeTeamCode: g.homeTeamCode,
|
||||
awayTeamCode: g.awayTeamCode,
|
||||
};
|
||||
|
||||
@ -23,7 +23,7 @@ function sideOf(game: Game, teamCode: string): "home" | "away" {
|
||||
async function loadWaitingGame(gameId: string): Promise<Game> {
|
||||
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;
|
||||
|
||||
@ -105,7 +105,10 @@ export async function createMe(
|
||||
}
|
||||
|
||||
const displayName = parseDisplayName(body.displayName);
|
||||
const favoriteTeamCode = parseEnum<TeamCode>(
|
||||
const favoriteTeamCode =
|
||||
body.favoriteTeamCode === undefined || body.favoriteTeamCode === null
|
||||
? undefined
|
||||
: parseEnum<TeamCode>(
|
||||
"favoriteTeamCode",
|
||||
body.favoriteTeamCode,
|
||||
TEAM_CODE_VALUES
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
};
|
||||
|
||||
@ -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가 없다", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user