131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
"""야구 일정 DB 반영 — (리그, KST 날짜, 팀쌍, 차수) 키 매칭. 워커가 매일 호출.
|
|
|
|
- 기존 경기: 키 매칭 → 시각/venue/투표시간 갱신 (결과·예측·crowd 보존)
|
|
- 신규 경기: 삽입 + crowd 초기화.
|
|
match_id = {LEAGUE}_{away}_{home}_{yyyymmdd} (더블헤더 2차전은 _2 접미)
|
|
- 취소(우천 등): 삭제하지 않고 status="cancelled" 로 보존 — 투표·예측 기록 유지,
|
|
정산·투표·AI 생성에서 제외. 소스가 취소를 번복하면 scheduled 로 복귀.
|
|
(보강 경기는 새 날짜의 신규 경기로 재등장)
|
|
- 더블헤더: 같은 (날짜, 팀쌍) 복수 경기를 시작시각순 seq(1,2)로 구분해 모두 등록.
|
|
- 투표창: 리그 공통 오픈 오프셋(vote_open_hours_before, 기본 -168h)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ..config import settings
|
|
from ..models import CrowdStats, Match
|
|
from ..teams_baseball import team_info
|
|
|
|
log = logging.getLogger("triplepick.baseball")
|
|
KST = timezone(timedelta(hours=9))
|
|
|
|
|
|
def baseball_opens_at(kickoff: datetime) -> datetime:
|
|
# 축구와 동일 정책(vote_open_hours_before=168h) — 야구는 일정을 7일치만
|
|
# 수집하므로 사실상 동기화 즉시 투표 오픈된다.
|
|
return kickoff - timedelta(hours=settings.vote_open_hours_before)
|
|
|
|
|
|
def baseball_lock_at(kickoff: datetime) -> datetime:
|
|
return kickoff - timedelta(minutes=settings.vote_lock_minutes_before)
|
|
|
|
|
|
def _date_of(m: Match) -> str:
|
|
return m.kickoff_at.astimezone(KST).strftime("%Y%m%d") if m.kickoff_at else ""
|
|
|
|
|
|
def match_seq(match_id: str) -> int:
|
|
"""match_id 의 더블헤더 차수. `..._20260722` → 1, `..._20260722_2` → 2."""
|
|
tail = match_id.rsplit("_", 1)[-1]
|
|
return int(tail) if len(tail) <= 2 and tail.isdigit() else 1
|
|
|
|
|
|
def assign_seq(records: list[dict]) -> None:
|
|
"""같은 (날짜, 팀쌍) 레코드에 시작시각순 seq(1,2,…)를 부여 — 더블헤더 구분."""
|
|
groups: dict[tuple, list[dict]] = {}
|
|
for rec in records:
|
|
groups.setdefault((rec["dateKst"], rec["teamA"], rec["teamB"]), []).append(rec)
|
|
for recs in groups.values():
|
|
recs.sort(key=lambda r: r["kickoffKst"])
|
|
for i, rec in enumerate(recs, start=1):
|
|
rec["seq"] = i
|
|
|
|
|
|
async def sync_baseball_schedule(
|
|
db: AsyncSession, league: str, records: list[dict]
|
|
) -> dict:
|
|
if not records:
|
|
return {"updated": 0, "inserted": 0, "skipped": 0, "removed": 0}
|
|
|
|
assign_seq(records)
|
|
|
|
existing = (
|
|
await db.execute(select(Match).where(Match.league == league))
|
|
).scalars().all()
|
|
by_key: dict[tuple, Match] = {}
|
|
for m in existing:
|
|
d, s = _date_of(m), match_seq(m.match_id)
|
|
by_key[(d, m.team_a_code, m.team_b_code, s)] = m
|
|
by_key[(d, m.team_b_code, m.team_a_code, s)] = m
|
|
|
|
updated = inserted = skipped = removed = 0
|
|
for rec in records:
|
|
a, b, d = rec["teamA"], rec["teamB"], rec["dateKst"]
|
|
seq = rec.get("seq", 1)
|
|
m = by_key.get((d, a, b, seq))
|
|
|
|
if rec.get("cancelled"):
|
|
# 소프트 취소 — 투표/예측 기록 보존, 정산·투표·AI 대상에서 제외.
|
|
if m is not None and m.result_outcome is None and m.status != "cancelled":
|
|
m.status = "cancelled"
|
|
removed += 1
|
|
continue
|
|
|
|
kickoff = datetime.fromisoformat(rec["kickoffKst"]).astimezone(timezone.utc)
|
|
if m is not None:
|
|
if m.result_outcome is not None:
|
|
skipped += 1
|
|
continue
|
|
if m.status == "cancelled":
|
|
m.status = "scheduled" # 취소 번복 — 시간 기준 상태는 tick 이 복원
|
|
m.kickoff_at = kickoff
|
|
m.opens_at = baseball_opens_at(kickoff)
|
|
m.lock_at = baseball_lock_at(kickoff)
|
|
if rec.get("venue"):
|
|
m.venue = rec["venue"]
|
|
updated += 1
|
|
else:
|
|
ta, tb = team_info(league, a), team_info(league, b)
|
|
match_id = f"{league.upper()}_{a}_{b}_{d}" + (f"_{seq}" if seq > 1 else "")
|
|
new = Match(
|
|
match_id=match_id,
|
|
league=league,
|
|
round_label="정규시즌",
|
|
group="",
|
|
team_a_name=ta["name"], team_a_short=ta["shortName"],
|
|
team_a_code=ta["code"], team_a_flag=ta["flag"],
|
|
team_b_name=tb["name"], team_b_short=tb["shortName"],
|
|
team_b_code=tb["code"], team_b_flag=tb["flag"],
|
|
venue=rec.get("venue", ""),
|
|
hook_text=f"{ta['shortName']} vs {tb['shortName']}",
|
|
kickoff_at=kickoff,
|
|
opens_at=baseball_opens_at(kickoff),
|
|
lock_at=baseball_lock_at(kickoff),
|
|
status="scheduled",
|
|
)
|
|
db.add(new)
|
|
db.add(CrowdStats(match_id=match_id, total=0, team_a_win=0, draw=0, team_b_win=0))
|
|
by_key[(d, a, b, seq)] = new
|
|
by_key[(d, b, a, seq)] = new
|
|
inserted += 1
|
|
|
|
await db.commit()
|
|
result = {"updated": updated, "inserted": inserted, "skipped": skipped, "removed": removed}
|
|
log.info("baseball sync(%s): %s", league, result)
|
|
return result
|