From 11dd968ced2383394d53660747efb5bc32f20836 Mon Sep 17 00:00:00 2001 From: jwkim Date: Thu, 27 Aug 2026 14:48:21 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=95=EC=82=B0=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EB=8F=99=EC=A0=81=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=E2=80=94=20=EC=98=A4=EB=9E=98=20=EB=B0=A9=EC=B9=98=EB=90=9C=20?= =?UTF-8?q?=EB=AF=B8=EC=A0=95=EC=82=B0=20=EA=B2=BD=EA=B8=B0=EA=B0=80=20?= =?UTF-8?q?=EC=98=81=EC=9B=90=ED=9E=88=20live=EB=A1=9C=20=EB=82=A8?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit settle_baseball 이 결과를 조회하는 윈도우(KBO/MLB=result_recheck_days 기본 3일, MLS=mls_days_back 기본 30일)가 고정폭이라, 최초 수집에 실패한 경기가 그 윈도우 밖으로 밀려나면 이후 영원히 매칭 대상에서 빠져 status=live로 고정되는 버그가 있었다(예: MLB_PHI_BAL_20260803, 8/2 이후 3주+ live 고착). 미정산 경기 중 가장 오래된 킥오프 날짜까지 덮도록 조회 범위를 매 틱 동적 계산(최대 120일 소급)하도록 수정. Co-Authored-By: Claude Fable 5 --- backend/app/services/baseball_fetch.py | 18 ++++++++++++------ backend/app/services/mls_espn.py | 14 +++++++++----- backend/app/worker.py | 19 ++++++++++++++----- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/backend/app/services/baseball_fetch.py b/backend/app/services/baseball_fetch.py index 6d35d1a..2711aaf 100644 --- a/backend/app/services/baseball_fetch.py +++ b/backend/app/services/baseball_fetch.py @@ -115,13 +115,19 @@ async def _fetch_mlb(days_back: int, days_ahead: int) -> list[dict]: # ── 공개 API ──────────────────────────────────────────────────── -async def fetch_baseball_schedule(league: str) -> list[dict]: - """리그 일정+결과 수집 (취소 포함). 실패 시 빈 리스트 — 기존 일정 유지.""" +async def fetch_baseball_schedule(league: str, days_back: int | None = None) -> list[dict]: + """리그 일정+결과 수집 (취소 포함). 실패 시 빈 리스트 — 기존 일정 유지. + + days_back 기본값은 result_recheck_days(짧은 재확인 윈도우). settle_baseball 은 + 미정산 경기 중 가장 오래된 날짜를 덮도록 더 큰 값을 넘겨, 오래 방치된 + 미정산 경기(수집 실패 등으로 첫 정산을 놓친 경기)도 결국 잡히게 한다. + """ + days_back = settings.result_recheck_days if days_back is None else days_back try: if league == "kbo": - records = await _fetch_kbo(settings.result_recheck_days, settings.baseball_days_ahead) + records = await _fetch_kbo(days_back, settings.baseball_days_ahead) elif league == "mlb": - records = await _fetch_mlb(settings.result_recheck_days, settings.baseball_days_ahead) + records = await _fetch_mlb(days_back, settings.baseball_days_ahead) else: return [] # 더블헤더 차수 부여 — sync·정산이 같은 seq 로 경기를 식별한다. @@ -135,6 +141,6 @@ async def fetch_baseball_schedule(league: str) -> list[dict]: return [] -async def fetch_baseball_results(league: str) -> list[dict]: +async def fetch_baseball_results(league: str, days_back: int | None = None) -> list[dict]: """확정 스코어만 → [{league, teamA, teamB, dateKst, scoreA, scoreB}].""" - return [r for r in await fetch_baseball_schedule(league) if "scoreA" in r] + return [r for r in await fetch_baseball_schedule(league, days_back) if "scoreA" in r] diff --git a/backend/app/services/mls_espn.py b/backend/app/services/mls_espn.py index 2915dde..7496c65 100644 --- a/backend/app/services/mls_espn.py +++ b/backend/app/services/mls_espn.py @@ -105,14 +105,18 @@ def _clean(rec: dict) -> dict: return {k: v for k, v in rec.items() if not k.startswith("_")} -async def fetch_mls_schedule() -> list[dict]: - """일정+결과 수집 (취소 포함). 실패 시 빈 리스트 — 기존 일정 유지.""" +async def fetch_mls_schedule(days_back: int | None = None) -> list[dict]: + """일정+결과 수집 (취소 포함). 실패 시 빈 리스트 — 기존 일정 유지. + + days_back 기본값은 mls_days_back. settle_baseball 이 미정산 경기 중 + 가장 오래된 날짜를 덮도록 더 큰 값을 넘길 수 있다(오래 방치된 미정산 경기 구제). + """ import httpx today = datetime.now(KST).date() # 시작 = 백필 윈도우(주말 위주 편성 공백 방지). ESPN dates 는 미국 동부 날짜라 # KST 범위를 놓치지 않게 하루 여유를 둔다. - start = today - timedelta(days=settings.mls_days_back + 1) + start = today - timedelta(days=(days_back if days_back is not None else settings.mls_days_back) + 1) end = today + timedelta(days=settings.baseball_days_ahead) try: async with httpx.AsyncClient(timeout=20) as c: @@ -130,9 +134,9 @@ async def fetch_mls_schedule() -> list[dict]: return [] -async def fetch_mls_results() -> list[dict]: +async def fetch_mls_results(days_back: int | None = None) -> list[dict]: """확정 스코어만 → 정산(settle) 입력.""" - return [r for r in await fetch_mls_schedule() if "scoreA" in r] + return [r for r in await fetch_mls_schedule(days_back) if "scoreA" in r] # ── 순위 (동/서부 컨퍼런스, 승점제) ───────────────────────────── diff --git a/backend/app/worker.py b/backend/app/worker.py index 936dba5..e429d0e 100644 --- a/backend/app/worker.py +++ b/backend/app/worker.py @@ -19,7 +19,7 @@ from __future__ import annotations import asyncio import logging -from datetime import timedelta +from datetime import timedelta, timezone from apscheduler.schedulers.asyncio import AsyncIOScheduler from sqlalchemy import select @@ -344,13 +344,22 @@ async def settle_baseball() -> None: ) ) ).scalars().all() - due = [m.match_id for m in pending if ensure_aware(m.kickoff_at) <= now] - if not due: + due_matches = [m for m in pending if ensure_aware(m.kickoff_at) <= now] + if not due_matches: continue + due = [m.match_id for m in due_matches] + # 미정산 경기 중 가장 오래된 킥오프까지 덮도록 조회 범위를 동적 확장. + # 기본 윈도우(며칠)만 쓰면 첫 수집에 실패한 경기가 윈도우 밖으로 + # 밀려나 영원히 "live"에 고정되는 문제가 있었다 — 최대 120일까지 소급. + oldest_kst_date = min( + ensure_aware(m.kickoff_at).astimezone(timezone(_KST)).date() + for m in due_matches + ) + days_back = min((now.astimezone(timezone(_KST)).date() - oldest_kst_date).days + 1, 120) results = ( - await mls_espn.fetch_mls_results() + await mls_espn.fetch_mls_results(days_back=days_back) if league == "mls" - else await fetch_baseball_results(league) + else await fetch_baseball_results(league, days_back=days_back) ) by_key: dict[tuple, tuple[int, int]] = {} for r in results: