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
hbyang 2026-06-12 09:41:36 +09:00
parent 140e15fb9f
commit 54ed8bf10d
4 changed files with 27 additions and 9 deletions

View File

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

View File

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

View File

@ -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()

View File

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