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_SOURCE=openfootball # openfootball(키 불필요) | football-data(토큰) | fallback
|
||||||
SCHEDULE_URL=https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json
|
SCHEDULE_URL=https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json
|
||||||
SCHEDULE_GROUP=Group A
|
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
|
FOOTBALL_DATA_COMPETITION=WC
|
||||||
|
# 결과(스코어) 소스 — 일정과 분리. 비우면 SCHEDULE_SOURCE 사용.
|
||||||
|
# openfootball 은 결과 미게시 → 자동 종료 쓰려면 football-data 권장.
|
||||||
|
RESULT_SOURCE=football-data
|
||||||
|
|
||||||
AI_GENERATE_HOUR=0 # 매일 KST 00:05 AI 예측 생성
|
AI_GENERATE_HOUR=0 # 매일 KST 00:05 AI 예측 생성
|
||||||
AI_GENERATE_MINUTE=5
|
AI_GENERATE_MINUTE=5
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,15 @@ class Settings(BaseSettings):
|
||||||
"https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json"
|
"https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json"
|
||||||
)
|
)
|
||||||
schedule_group: str = "Group A" # 추적할 그룹 (openfootball 라벨)
|
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"
|
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 예측 생성 시각 (KST). 기능정의서: 매일 0시 1회 생성.
|
||||||
ai_generate_hour: int = 0
|
ai_generate_hour: int = 0
|
||||||
|
|
|
||||||
|
|
@ -177,8 +177,12 @@ async def _results_football_data() -> list[dict]:
|
||||||
|
|
||||||
|
|
||||||
async def fetch_results() -> list[dict]:
|
async def fetch_results() -> list[dict]:
|
||||||
"""확정된 경기 스코어 수집 → [{teamA, teamB, scoreA, scoreB}]. 실패 시 빈 리스트."""
|
"""확정된 경기 스코어 수집 → [{teamA, teamB, scoreA, scoreB}]. 실패 시 빈 리스트.
|
||||||
src = settings.schedule_source.lower()
|
|
||||||
|
소스는 result_source(없으면 schedule_source). openfootball 은 결과 미게시라
|
||||||
|
자동 종료에는 football-data 를 권장.
|
||||||
|
"""
|
||||||
|
src = settings.effective_result_source
|
||||||
try:
|
try:
|
||||||
if src == "openfootball":
|
if src == "openfootball":
|
||||||
out = await _results_openfootball()
|
out = await _results_openfootball()
|
||||||
|
|
|
||||||
|
|
@ -145,19 +145,23 @@ async def settle_matches() -> None:
|
||||||
|
|
||||||
if due:
|
if due:
|
||||||
results = await fetch_results()
|
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
|
settled = 0
|
||||||
async with SessionLocal() as db:
|
async with SessionLocal() as db:
|
||||||
for mid in due:
|
for mid in due:
|
||||||
m = await db.get(Match, mid)
|
m = await db.get(Match, mid)
|
||||||
if m is None or m.result_outcome is not None:
|
if m is None or m.result_outcome is not None:
|
||||||
continue
|
continue
|
||||||
r = by_pair.get((m.team_a_code, m.team_b_code))
|
sc = by_pair.get((m.team_a_code, m.team_b_code))
|
||||||
if not r:
|
if not sc:
|
||||||
continue
|
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
|
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:
|
if settled == 0:
|
||||||
log.info("settle: %d경기 정산 대기 — 외부 스코어 아직 없음", len(due))
|
log.info("settle: %d경기 정산 대기 — 외부 스코어 아직 없음", len(due))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue