feat(settle): 결과 소스를 일정 소스와 분리(RESULT_SOURCE) + 순서 무관 매칭
- RESULT_SOURCE(비우면 SCHEDULE_SOURCE) 추가 — 일정=openfootball, 결과=football-data 가능 - fetch_results 가 result_source 사용 - 자동 정산 매칭을 홈/원정 순서 무관하게 보강(소스 순서 달라도 스코어 정합) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>develop
parent
140e15fb9f
commit
54ed8bf10d
|
|
@ -31,8 +31,11 @@ SCHEDULE_SYNC_MINUTE=0
|
|||
SCHEDULE_SOURCE=openfootball # openfootball(키 불필요) | football-data(토큰) | fallback
|
||||
SCHEDULE_URL=https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json
|
||||
SCHEDULE_GROUP=Group A
|
||||
FOOTBALL_DATA_TOKEN= # SCHEDULE_SOURCE=football-data 일 때
|
||||
FOOTBALL_DATA_TOKEN= # football-data 사용 시 토큰 (football-data.org/client/register)
|
||||
FOOTBALL_DATA_COMPETITION=WC
|
||||
# 결과(스코어) 소스 — 일정과 분리. 비우면 SCHEDULE_SOURCE 사용.
|
||||
# openfootball 은 결과 미게시 → 자동 종료 쓰려면 football-data 권장.
|
||||
RESULT_SOURCE=football-data
|
||||
|
||||
AI_GENERATE_HOUR=0 # 매일 KST 00:05 AI 예측 생성
|
||||
AI_GENERATE_MINUTE=5
|
||||
|
|
|
|||
|
|
@ -67,8 +67,15 @@ class Settings(BaseSettings):
|
|||
"https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json"
|
||||
)
|
||||
schedule_group: str = "Group A" # 추적할 그룹 (openfootball 라벨)
|
||||
football_data_token: str = "" # schedule_source=football-data 일 때
|
||||
football_data_token: str = "" # football-data 사용 시 토큰
|
||||
football_data_competition: str = "WC"
|
||||
# 결과(스코어) 소스 — 일정 소스와 분리. 빈값이면 schedule_source 사용.
|
||||
# openfootball 은 결과 미게시라, 자동 종료를 쓰려면 football-data 권장.
|
||||
result_source: str = ""
|
||||
|
||||
@property
|
||||
def effective_result_source(self) -> str:
|
||||
return (self.result_source or self.schedule_source).lower()
|
||||
|
||||
# 매일 AI 예측 생성 시각 (KST). 기능정의서: 매일 0시 1회 생성.
|
||||
ai_generate_hour: int = 0
|
||||
|
|
|
|||
|
|
@ -177,8 +177,12 @@ async def _results_football_data() -> list[dict]:
|
|||
|
||||
|
||||
async def fetch_results() -> list[dict]:
|
||||
"""확정된 경기 스코어 수집 → [{teamA, teamB, scoreA, scoreB}]. 실패 시 빈 리스트."""
|
||||
src = settings.schedule_source.lower()
|
||||
"""확정된 경기 스코어 수집 → [{teamA, teamB, scoreA, scoreB}]. 실패 시 빈 리스트.
|
||||
|
||||
소스는 result_source(없으면 schedule_source). openfootball 은 결과 미게시라
|
||||
자동 종료에는 football-data 를 권장.
|
||||
"""
|
||||
src = settings.effective_result_source
|
||||
try:
|
||||
if src == "openfootball":
|
||||
out = await _results_openfootball()
|
||||
|
|
|
|||
|
|
@ -145,19 +145,23 @@ async def settle_matches() -> None:
|
|||
|
||||
if due:
|
||||
results = await fetch_results()
|
||||
by_pair = {(r["teamA"], r["teamB"]): r for r in results}
|
||||
# 정방향/역방향 키 모두 등록 — 소스의 홈/원정 순서가 우리와 달라도 매칭.
|
||||
by_pair: dict[tuple, tuple[int, int]] = {}
|
||||
for r in results:
|
||||
by_pair[(r["teamA"], r["teamB"])] = (r["scoreA"], r["scoreB"])
|
||||
by_pair[(r["teamB"], r["teamA"])] = (r["scoreB"], r["scoreA"]) # 뒤집어 저장
|
||||
settled = 0
|
||||
async with SessionLocal() as db:
|
||||
for mid in due:
|
||||
m = await db.get(Match, mid)
|
||||
if m is None or m.result_outcome is not None:
|
||||
continue
|
||||
r = by_pair.get((m.team_a_code, m.team_b_code))
|
||||
if not r:
|
||||
sc = by_pair.get((m.team_a_code, m.team_b_code))
|
||||
if not sc:
|
||||
continue
|
||||
await apply_result(db, m, r["scoreA"], r["scoreB"]) # 채점+집계+commit
|
||||
await apply_result(db, m, sc[0], sc[1]) # 우리 팀A/팀B 기준으로 채점+집계+commit
|
||||
settled += 1
|
||||
log.info("settle: %s 자동 정산 %d-%d", mid, r["scoreA"], r["scoreB"])
|
||||
log.info("settle: %s 자동 정산 %d-%d", mid, sc[0], sc[1])
|
||||
if settled == 0:
|
||||
log.info("settle: %d경기 정산 대기 — 외부 스코어 아직 없음", len(due))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue