"""픽 제출 API — 검증 · 중복방지(1회 수정) · crowd 원자적 증분 · 매칭 모델 계산.""" from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from ..database import get_db from ..domain import crowd_out, is_open_for_voting from ..models import AIPrediction, CrowdStats, Match, UserPrediction from ..scoring import outcome_of from ..schemas import SubmitPredictionIn, SubmitPredictionOut router = APIRouter(prefix="/api/predictions", tags=["predictions"]) _COL = {"TEAM_A_WIN": "team_a_win", "DRAW": "draw", "TEAM_B_WIN": "team_b_win"} async def _adjust_crowd( db: AsyncSession, match_id: str, *, add: str | None, remove: str | None ) -> None: """crowd_stats 원자적 증분/보정 (Postgres UPDATE).""" values: dict = {} if add: col = _COL[add] values[col] = CrowdStats.__table__.c[col] + 1 if not remove: # 신규 제출이면 total +1 values["total"] = CrowdStats.total + 1 if remove and remove != add: col = _COL[remove] values[col] = CrowdStats.__table__.c[col] - 1 if values: await db.execute( update(CrowdStats).where(CrowdStats.match_id == match_id).values(**values) ) @router.post("", response_model=SubmitPredictionOut) async def submit_prediction( body: SubmitPredictionIn, db: AsyncSession = Depends(get_db) ) -> SubmitPredictionOut: match = ( await db.execute( select(Match) .where(Match.match_id == body.matchId) .options(selectinload(Match.predictions)) ) ).scalars().first() if not match: raise HTTPException(status_code=404, detail="MATCH_NOT_FOUND") if not is_open_for_voting(match): # 마감/종료/오픈전 구분 if match.result_outcome is not None: raise HTTPException(status_code=409, detail="MATCH_FINISHED") raise HTTPException(status_code=423, detail="MATCH_LOCKED") # outcome 은 스코어에서 도출 (입력과 불일치해도 스코어 기준으로 정규화) outcome = outcome_of(body.scoreA, body.scoreB) existing = ( await db.execute( select(UserPrediction).where( UserPrediction.match_id == body.matchId, UserPrediction.device_id == body.deviceId, ) ) ).scalars().first() if existing: # 마감 전 1회 수정: 분포 보정 (이전 outcome 제거, 새 outcome 추가) await _adjust_crowd(db, body.matchId, add=outcome, remove=existing.outcome) existing.outcome = outcome existing.score_a = body.scoreA existing.score_b = body.scoreB if body.email: existing.email = str(body.email) existing.notify = body.notify pred = existing else: await _adjust_crowd(db, body.matchId, add=outcome, remove=None) pred = UserPrediction( match_id=body.matchId, device_id=body.deviceId, outcome=outcome, score_a=body.scoreA, score_b=body.scoreB, email=str(body.email) if body.email else None, notify=body.notify, ) db.add(pred) await db.commit() # 매칭 AI 모델 (같은 outcome) + 정확스코어 일치 여부 matched: list[str] = [] exact = False for p in match.predictions: if p.outcome == outcome: matched.append(p.model) if p.score_a == body.scoreA and p.score_b == body.scoreB: exact = True crowd = ( await db.execute( select(CrowdStats).where(CrowdStats.match_id == body.matchId) ) ).scalars().first() return SubmitPredictionOut( ok=True, predictionId=f"{body.matchId}_{body.deviceId}", matchedModels=matched, # type: ignore[arg-type] exactMatch=exact, crowd=crowd_out(crowd, body.matchId), )