"""유저별 포인트 누적 — grade_prediction 의 (key, value) 를 user_points 에 반영. 채점(apply_result)에서 호출. 결과 정정(재채점)에도 안전하도록 영향받은 이메일의 채점 가능한 픽 전체를 재집계해 upsert 한다(idempotent — 몇 번을 다시 돌려도 같은 결과, 증분 방식의 이중 누적 위험 없음). """ from __future__ import annotations import logging from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from ..domain import now_utc from ..models import Match, UserPoints, UserPrediction from ..scoring import grade_prediction log = logging.getLogger("triplepick.points") # scoring.json 의 key → user_points 등급별 횟수 컬럼 _KEY_TO_COL = { "score_exact": "exact_count", "score_close": "close_count", "score_outcome": "outcome_count", "score_partial": "partial_count", "score_miss": "miss_count", } def _empty() -> dict: return { "total_points": 0, "exact_count": 0, "close_count": 0, "outcome_count": 0, "partial_count": 0, "miss_count": 0, "matches_played": 0, "first_scored_at": None, } async def accumulate_user_points( db: AsyncSession, emails: set[str | None] ) -> int: """이메일별 누적 포인트 재집계 → user_points upsert. 갱신 행 수 반환. 커밋은 호출자(apply_result) 책임 — 채점과 누적이 한 트랜잭션으로 묶인다. """ targets = {e.strip().lower() for e in emails if e and e.strip()} if not targets: return 0 rows = ( await db.execute( select(UserPrediction, Match) .join(Match, UserPrediction.match_id == Match.match_id) .where( func.lower(UserPrediction.email).in_(targets), Match.result_score_a.is_not(None), Match.result_score_b.is_not(None), ) ) ).all() agg: dict[str, dict] = {e: _empty() for e in targets} for pick, match in rows: key, value = grade_prediction( pick.score_a, pick.score_b, match.result_score_a, match.result_score_b ) t = agg[pick.email.strip().lower()] t["total_points"] += value t[_KEY_TO_COL[key]] += 1 t["matches_played"] += 1 ts = pick.scored_at or pick.created_at if ts and (t["first_scored_at"] is None or ts < t["first_scored_at"]): t["first_scored_at"] = ts existing = { up.email: up for up in ( await db.execute(select(UserPoints).where(UserPoints.email.in_(targets))) ).scalars() } for email, t in agg.items(): row = existing.get(email) if row is None: row = UserPoints(email=email) db.add(row) for col, val in t.items(): setattr(row, col, val) row.updated_at = now_utc() log.info( "user_points: %d명 누적 갱신 (%s)", len(agg), ", ".join(f"{e}={t['total_points']}p" for e, t in agg.items()), ) return len(agg)