정산 조회 범위 동적 확장 — 오래 방치된 미정산 경기가 영원히 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 ────────────────────────────────────────────────────
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]

View File

@ -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]
# ── 순위 (동/서부 컨퍼런스, 승점제) ─────────────────────────────

View File

@ -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: