From 54ed8bf10d5bf2672dde8ab861bba2069543a7e9 Mon Sep 17 00:00:00 2001 From: hbyang Date: Fri, 12 Jun 2026 09:41:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(settle):=20=EA=B2=B0=EA=B3=BC=20=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EC=9D=BC=EC=A0=95=20=EC=86=8C=EC=8A=A4?= =?UTF-8?q?=EC=99=80=20=EB=B6=84=EB=A6=AC(RESULT=5FSOURCE)=20+=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EB=AC=B4=EA=B4=80=20=EB=A7=A4=EC=B9=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RESULT_SOURCE(비우면 SCHEDULE_SOURCE) 추가 — 일정=openfootball, 결과=football-data 가능 - fetch_results 가 result_source 사용 - 자동 정산 매칭을 홈/원정 순서 무관하게 보강(소스 순서 달라도 스코어 정합) Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/.env.example | 5 ++++- backend/app/config.py | 9 ++++++++- backend/app/services/schedule_fetch.py | 8 ++++++-- backend/app/worker.py | 14 +++++++++----- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index dc32d54..4ed17b1 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -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 diff --git a/backend/app/config.py b/backend/app/config.py index 1d42fa3..59c6d49 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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 diff --git a/backend/app/services/schedule_fetch.py b/backend/app/services/schedule_fetch.py index b746f6b..a64df1b 100644 --- a/backend/app/services/schedule_fetch.py +++ b/backend/app/services/schedule_fetch.py @@ -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() diff --git a/backend/app/worker.py b/backend/app/worker.py index e3823f2..43d4685 100644 --- a/backend/app/worker.py +++ b/backend/app/worker.py @@ -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))