From c9244bc42da66ba3a209d273470998fffde4673e Mon Sep 17 00:00:00 2001 From: hbyang Date: Fri, 12 Jun 2026 10:51:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(predictions):=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=EB=8F=84=20=EC=A4=91=EB=B3=B5=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80=20(=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9A=B0?= =?UTF-8?q?=EC=84=A0=20=EC=8B=9D=EB=B3=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 같은 이메일은 같은 경기에 1픽만 — 다른 기기에서 같은 이메일로 제출 시 새 픽 추가 대신 기존 픽 수정 + crowd 보정. 이메일은 소문자 정규화 저장. 모델에 (match_id, email) 유니크 제약 추가(신규 DB 적용, 기존 테이블은 앱 로직 보장). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/models.py | 3 +++ backend/app/routers/predictions.py | 36 +++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/backend/app/models.py b/backend/app/models.py index 1948adc..41b860b 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -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) diff --git a/backend/app/routers/predictions.py b/backend/app/routers/predictions.py index 2beabaf..155b754 100644 --- a/backend/app/routers/predictions.py +++ b/backend/app/routers/predictions.py @@ -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,15 +63,28 @@ async def submit_prediction( # outcome 은 스코어에서 도출 (입력과 불일치해도 스코어 기준으로 정규화) outcome = outcome_of(body.scoreA, body.scoreB) + email = str(body.email).strip().lower() if body.email else None - existing = ( - await db.execute( - select(UserPrediction).where( - UserPrediction.match_id == body.matchId, - UserPrediction.device_id == body.deviceId, + # 중복 식별: ① 이메일(있으면 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() + ).scalars().first() + if existing is None: + existing = ( + await db.execute( + select(UserPrediction).where( + UserPrediction.match_id == body.matchId, + UserPrediction.device_id == body.deviceId, + ) + ) + ).scalars().first() if existing: # 마감 전 1회 수정: 분포 보정 (이전 outcome 제거, 새 outcome 추가) @@ -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)