Handle tie games in KBO schedule parsing and result processing.

- KBO 경기 결과 파싱 시 무승부(`draw`) 상황의 점수도 추출할 수 있도록 정규표현식을 개선하여 경기 상태가 'scheduled'로 오분류되는 문제를 해결했습니다.
- 승리 팀 코드(`winningTeamCode`)가 없는 무승부 경기에서도 결과 처리가 정상적으로 수행되도록 서비스 레이어의 예외 처리를 수정했습니다.
- 무승부 시 어느 팀에 투표했더라도 모두 정답(`result: true`)으로 처리되도록 투표 결과 정산 및 자가 치유(reconcile) 로직을 업데이트했습니다.
This commit is contained in:
윤정민 2026-05-07 16:47:48 +09:00
parent c94ce69e4f
commit 1a5f6625c0
3 changed files with 16 additions and 10 deletions

View File

@ -131,12 +131,14 @@ function parsePlayCell(html: string): {
const homeTeamCode = toTeamCode(teamMatch[3]);
const emContent = teamMatch[2];
// Check for scores inside <em>
const scoreRegex = /<span class="(win|lose)">(\d+)<\/span>/g;
const scores: { cls: string; val: number }[] = [];
// Check for scores inside <em>. 무승부 시 KBO는 `class="draw"`(또는 win/lose가 아닌 다른 값)을
// 쓰므로 win/lose만 매칭하면 점수가 0건으로 잡혀 scheduled로 분류된다. 클래스 값과 무관하게
// span 안의 숫자만 추출한다.
const scoreRegex = /<span class="[^"]*">(\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",
};
}

View File

@ -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) {

View File

@ -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<string, unknown> = {};
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;
}