정산 조회 범위 동적 확장 — 오래 방치된 미정산 경기가 영원히 live로 남는 버그 수정

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 <noreply@anthropic.com>
This commit is contained in:
jwkim 2026-08-27 14:48:21 +09:00
parent 746eaced80
commit 11dd968ced
3 changed files with 35 additions and 16 deletions

View File

@ -115,13 +115,19 @@ async def _fetch_mlb(days_back: int, days_ahead: int) -> list[dict]:
# ── 공개 API ──────────────────────────────────────────────────── # ── 공개 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: try:
if league == "kbo": 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": 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: else:
return [] return []
# 더블헤더 차수 부여 — sync·정산이 같은 seq 로 경기를 식별한다. # 더블헤더 차수 부여 — sync·정산이 같은 seq 로 경기를 식별한다.
@ -135,6 +141,6 @@ async def fetch_baseball_schedule(league: str) -> list[dict]:
return [] 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}].""" """확정 스코어만 → [{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]

View File

@ -105,14 +105,18 @@ def _clean(rec: dict) -> dict:
return {k: v for k, v in rec.items() if not k.startswith("_")} 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 import httpx
today = datetime.now(KST).date() today = datetime.now(KST).date()
# 시작 = 백필 윈도우(주말 위주 편성 공백 방지). ESPN dates 는 미국 동부 날짜라 # 시작 = 백필 윈도우(주말 위주 편성 공백 방지). ESPN dates 는 미국 동부 날짜라
# KST 범위를 놓치지 않게 하루 여유를 둔다. # 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) end = today + timedelta(days=settings.baseball_days_ahead)
try: try:
async with httpx.AsyncClient(timeout=20) as c: async with httpx.AsyncClient(timeout=20) as c:
@ -130,9 +134,9 @@ async def fetch_mls_schedule() -> list[dict]:
return [] return []
async def fetch_mls_results() -> list[dict]: async def fetch_mls_results(days_back: int | None = None) -> list[dict]:
"""확정 스코어만 → 정산(settle) 입력.""" """확정 스코어만 → 정산(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]
# ── 순위 (동/서부 컨퍼런스, 승점제) ───────────────────────────── # ── 순위 (동/서부 컨퍼런스, 승점제) ─────────────────────────────

View File

@ -19,7 +19,7 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
from datetime import timedelta from datetime import timedelta, timezone
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from sqlalchemy import select from sqlalchemy import select
@ -344,13 +344,22 @@ async def settle_baseball() -> None:
) )
) )
).scalars().all() ).scalars().all()
due = [m.match_id for m in pending if ensure_aware(m.kickoff_at) <= now] due_matches = [m for m in pending if ensure_aware(m.kickoff_at) <= now]
if not due: if not due_matches:
continue 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 = ( results = (
await mls_espn.fetch_mls_results() await mls_espn.fetch_mls_results(days_back=days_back)
if league == "mls" 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]] = {} by_key: dict[tuple, tuple[int, int]] = {}
for r in results: for r in results: