feat(predictions): 이메일도 중복 투표 방지 (이메일 우선 식별)

같은 이메일은 같은 경기에 1픽만 — 다른 기기에서 같은 이메일로 제출 시
새 픽 추가 대신 기존 픽 수정 + crowd 보정. 이메일은 소문자 정규화 저장.
모델에 (match_id, email) 유니크 제약 추가(신규 DB 적용, 기존 테이블은 앱 로직 보장).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
develop
hbyang 2026-06-12 10:51:42 +09:00
parent 86290a7c69
commit c9244bc42d
2 changed files with 28 additions and 11 deletions

View File

@ -128,6 +128,9 @@ class UserPrediction(Base):
__tablename__ = "user_predictions"
__table_args__ = (
UniqueConstraint("match_id", "device_id", name="uq_match_device"),
# 같은 이메일은 같은 경기에 1픽만 (NULL 이메일은 다수 허용 — NULL 은 서로 구별).
# 신규 DB 에만 자동 적용. 기존 테이블은 앱 로직(이메일 우선 식별)으로 보장.
UniqueConstraint("match_id", "email", name="uq_match_email"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select, update
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@ -63,7 +63,20 @@ async def submit_prediction(
# outcome 은 스코어에서 도출 (입력과 불일치해도 스코어 기준으로 정규화)
outcome = outcome_of(body.scoreA, body.scoreB)
email = str(body.email).strip().lower() if body.email else None
# 중복 식별: ① 이메일(있으면 1차 신원 — 다른 기기여도 동일인) ② 기기(deviceId)
existing = None
if email:
existing = (
await db.execute(
select(UserPrediction).where(
UserPrediction.match_id == body.matchId,
func.lower(UserPrediction.email) == email,
)
)
).scalars().first()
if existing is None:
existing = (
await db.execute(
select(UserPrediction).where(
@ -79,8 +92,9 @@ async def submit_prediction(
existing.outcome = outcome
existing.score_a = body.scoreA
existing.score_b = body.scoreB
if body.email:
existing.email = str(body.email)
existing.device_id = body.deviceId # 최신 제출 기기로 갱신
if email:
existing.email = email
existing.notify = body.notify
pred = existing
else:
@ -91,7 +105,7 @@ async def submit_prediction(
outcome=outcome,
score_a=body.scoreA,
score_b=body.scoreB,
email=str(body.email) if body.email else None,
email=email,
notify=body.notify,
)
db.add(pred)