From 1a5f6625c07f2ce1828b117a8623f4d92770aa56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Thu, 7 May 2026 16:47:48 +0900 Subject: [PATCH] Handle tie games in KBO schedule parsing and result processing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KBO 경기 결과 파싱 시 무승부(`draw`) 상황의 점수도 추출할 수 있도록 정규표현식을 개선하여 경기 상태가 'scheduled'로 오분류되는 문제를 해결했습니다. - 승리 팀 코드(`winningTeamCode`)가 없는 무승부 경기에서도 결과 처리가 정상적으로 수행되도록 서비스 레이어의 예외 처리를 수정했습니다. - 무승부 시 어느 팀에 투표했더라도 모두 정답(`result: true`)으로 처리되도록 투표 결과 정산 및 자가 치유(reconcile) 로직을 업데이트했습니다. --- src/kbo/schedule.ts | 14 ++++++++------ src/scheduled/dailyArchive.ts | 7 +++++-- src/services/gameResultService.ts | 5 +++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/kbo/schedule.ts b/src/kbo/schedule.ts index 7d0a780..9bf4bac 100644 --- a/src/kbo/schedule.ts +++ b/src/kbo/schedule.ts @@ -131,12 +131,14 @@ function parsePlayCell(html: string): { const homeTeamCode = toTeamCode(teamMatch[3]); const emContent = teamMatch[2]; - // Check for scores inside - const scoreRegex = /(\d+)<\/span>/g; - const scores: { cls: string; val: number }[] = []; + // Check for scores inside . 무승부 시 KBO는 `class="draw"`(또는 win/lose가 아닌 다른 값)을 + // 쓰므로 win/lose만 매칭하면 점수가 0건으로 잡혀 scheduled로 분류된다. 클래스 값과 무관하게 + // span 안의 숫자만 추출한다. + const scoreRegex = /(\d+)<\/span>/g; + const scores: number[] = []; let sm: RegExpExecArray | null; while ((sm = scoreRegex.exec(emContent)) !== null) { - scores.push({ cls: sm[1], val: parseInt(sm[2], 10) }); + scores.push(parseInt(sm[1], 10)); } if (scores.length === 2) { @@ -144,8 +146,8 @@ function parsePlayCell(html: string): { return { awayTeamCode, homeTeamCode, - awayScore: scores[0].val, - homeScore: scores[1].val, + awayScore: scores[0], + homeScore: scores[1], status: "completed", }; } diff --git a/src/scheduled/dailyArchive.ts b/src/scheduled/dailyArchive.ts index c98bfef..996a1b9 100644 --- a/src/scheduled/dailyArchive.ts +++ b/src/scheduled/dailyArchive.ts @@ -50,12 +50,15 @@ async function reconcileDayVotes( } // 트리거가 실패/유실되었거나 배포 전 상태 전이가 일어난 경우의 자가치유 경로. - if (game.status === "completed" && game.winningTeamCode) { + // `winningTeamCode`가 없으면 무승부 — `processGameEndWithGame`이 양 팀 투표 모두 + // result=true로 처리한다. + if (game.status === "completed") { try { await processGameEndWithGame(gameId, game); + const isDraw = !game.winningTeamCode; result[gameId] = { team: vote.team, - result: vote.team === game.winningTeamCode, + result: isDraw ? true : vote.team === game.winningTeamCode, }; logger.info(`reconcile: judged ${gameId} on the fly (uid=${uid})`); } catch (err) { diff --git a/src/services/gameResultService.ts b/src/services/gameResultService.ts index 5c7dc64..37cdf3a 100644 --- a/src/services/gameResultService.ts +++ b/src/services/gameResultService.ts @@ -18,17 +18,18 @@ export async function processGameEndWithGame( gameId: string, game: Game ): Promise<{ processed: number }> { - if (!game.winningTeamCode) throw new HttpError(400, "winningTeamCode not set"); 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); const uids = Object.keys(votes); + // 무승부(`winningTeamCode` 없음): 양 팀 투표 모두 적중(`result: true`)으로 처리. + const isDraw = !game.winningTeamCode; const rtdbUpdates: Record = {}; for (const uid of uids) { const voted = votes[uid].team; - const result = voted === game.winningTeamCode; + const result = isDraw ? true : voted === game.winningTeamCode; rtdbUpdates[`/userVotes/${uid}/${date}/${gameId}/team`] = voted; rtdbUpdates[`/userVotes/${uid}/${date}/${gameId}/result`] = result; }