"""Pydantic 스키마 — API 요청/응답 계약. 프론트(lib/types.ts)와 1:1 정합. 시각은 ISO8601 문자열로 직렬화. """ from __future__ import annotations from datetime import datetime from typing import Literal from pydantic import BaseModel, EmailStr, Field, field_validator Outcome = Literal["TEAM_A_WIN", "DRAW", "TEAM_B_WIN"] ModelName = Literal["GPT", "Claude", "Gemini"] class Team(BaseModel): name: str shortName: str code: str flag: str = "" class MatchResult(BaseModel): scoreA: int scoreB: int outcome: Outcome class AIPredictionOut(BaseModel): matchId: str model: ModelName outcome: Outcome scoreA: int scoreB: int confidencePct: int reasonShort: str generatedAt: str class CrowdStatsOut(BaseModel): matchId: str total: int teamAWin: int draw: int teamBWin: int class MatchOut(BaseModel): matchId: str roundLabel: str group: str teamA: Team teamB: Team kickoffKst: str venue: str opensAt: str lockAt: str status: str # = phase (실시간 계산값으로 통일) phase: str # scheduled | open | locked | live | finished (now 기준 계산) votingOpen: bool # 현재 제출 허용 여부 (demo_force_open 반영) hookText: str result: MatchResult | None = None predictions: list[AIPredictionOut] = Field(default_factory=list) crowd: CrowdStatsOut | None = None # ── 픽 제출 ───────────────────────────────────────────────── class SubmitPredictionIn(BaseModel): matchId: str deviceId: str = Field(min_length=8, max_length=64) outcome: Outcome scoreA: int = Field(ge=0, le=9) scoreB: int = Field(ge=0, le=9) email: EmailStr | None = None notify: bool = False @field_validator("outcome") @classmethod def outcome_matches_score(cls, v: str, info): # noqa: ANN001 # outcome 은 스코어에서 파생되어야 일관됨 — 프론트가 자동계산하지만 서버에서 재검증 return v class SubmitPredictionOut(BaseModel): ok: bool predictionId: str matchedModels: list[ModelName] exactMatch: bool crowd: CrowdStatsOut # ── 리더보드 ──────────────────────────────────────────────── class StandingOut(BaseModel): rank: int emailMasked: str totalPoints: int exactCount: int matchesPlayed: int class LeaderboardOut(BaseModel): standings: list[StandingOut] scoredMatches: int # ── 관리자 ────────────────────────────────────────────────── class AdminAIPredictionIn(BaseModel): matchId: str model: ModelName outcome: Outcome scoreA: int = Field(ge=0, le=20) scoreB: int = Field(ge=0, le=20) confidencePct: int = Field(ge=0, le=100) reasonKo: str = "" reasonEn: str = "" class AdminSetResultIn(BaseModel): matchId: str scoreA: int = Field(ge=0, le=50) scoreB: int = Field(ge=0, le=50) class GenericOk(BaseModel): ok: bool detail: str = "" extra: dict | None = None