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" __tablename__ = "user_predictions"
__table_args__ = ( __table_args__ = (
UniqueConstraint("match_id", "device_id", name="uq_match_device"), 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) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException 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.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@ -63,7 +63,20 @@ async def submit_prediction(
# outcome 은 스코어에서 도출 (입력과 불일치해도 스코어 기준으로 정규화) # outcome 은 스코어에서 도출 (입력과 불일치해도 스코어 기준으로 정규화)
outcome = outcome_of(body.scoreA, body.scoreB) 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 = ( existing = (
await db.execute( await db.execute(
select(UserPrediction).where( select(UserPrediction).where(
@ -79,8 +92,9 @@ async def submit_prediction(
existing.outcome = outcome existing.outcome = outcome
existing.score_a = body.scoreA existing.score_a = body.scoreA
existing.score_b = body.scoreB existing.score_b = body.scoreB
if body.email: existing.device_id = body.deviceId # 최신 제출 기기로 갱신
existing.email = str(body.email) if email:
existing.email = email
existing.notify = body.notify existing.notify = body.notify
pred = existing pred = existing
else: else:
@ -91,7 +105,7 @@ async def submit_prediction(
outcome=outcome, outcome=outcome,
score_a=body.scoreA, score_a=body.scoreA,
score_b=body.scoreB, score_b=body.scoreB,
email=str(body.email) if body.email else None, email=email,
notify=body.notify, notify=body.notify,
) )
db.add(pred) db.add(pred)