diff --git a/.gitignore b/.gitignore index 12067c9..bd5379a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ venv/ # docker volume (로컬) pgdata/ +# 로컬 검증 전용 compose (서버에 가면 안 됨) +docker-compose.local.yml + # misc .DS_Store *.pem diff --git a/AGENTS.md b/AGENTS.md index 63bf1d7..d31cc36 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ frontend/src/ ## 시간/스케줄 규칙 (워커가 자동 처리) - 투표 오픈 = 킥오프 − 120h(D-5, `VOTE_OPEN_HOURS_BEFORE`) -- 투표 마감 = 킥오프 정각(`VOTE_LOCK_MINUTES_BEFORE=0`) +- 투표 마감 = 킥오프 5분 전(`VOTE_LOCK_MINUTES_BEFORE=5`) - 상태 전이: scheduled → open → locked → finished (`STATUS_TICK_SECONDS` 주기) - AI 예측 생성: 매일 KST `AI_GENERATE_HOUR:MINUTE` (실 LLM API 호출) - 결과 메일: 경기 종료 후 `RESULT_EMAIL_DELAY_MINUTES`(기본 180=3h) 경과 시 발송 diff --git a/README.md b/README.md index f6abf99..4d8fdec 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ AI/이메일 키가 없어도 전체 플로우(예측·제출·채점·리더보 | 작업 | 시점 | 설정 | |---|---|---| | 투표 오픈 | 킥오프 − 48h (D-2) | `VOTE_OPEN_HOURS_BEFORE` | -| 투표 마감 | 킥오프 정각 | `VOTE_LOCK_MINUTES_BEFORE` | +| 투표 마감 | 킥오프 − 5분 | `VOTE_LOCK_MINUTES_BEFORE` | | 상태 전이 (scheduled→open→locked→finished) | 주기 틱 | `STATUS_TICK_SECONDS` (기본 60s) | | AI 예측 생성 (3모델 실 API 호출) | 매일 KST 00:05 | `AI_GENERATE_HOUR` / `_MINUTE` | | 결과 메일 발송 (구독자) | 경기 종료 +3h | `RESULT_EMAIL_DELAY_MINUTES` | diff --git a/backend/.env.example b/backend/.env.example index 4ed17b1..f68d3b7 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -21,7 +21,7 @@ PUBLIC_ORIGIN=http://localhost:8080 # 투표 윈도우 (도메인 규칙) VOTE_OPEN_HOURS_BEFORE=48 # 오픈 = 킥오프 D-2 -VOTE_LOCK_MINUTES_BEFORE=60 # 마감 = 킥오프 1시간 전 +VOTE_LOCK_MINUTES_BEFORE=5 # 마감 = 킥오프 5분 전 DEMO_FORCE_OPEN=true # 운영 배포 시 false # 스케줄링 서버 (워커) diff --git a/backend/app/config.py b/backend/app/config.py index b2974b3..53559dd 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -52,8 +52,8 @@ class Settings(BaseSettings): # ── 투표 윈도우 (도메인 규칙) ───────────────────────────── # 오픈 = 킥오프 - VOTE_OPEN_HOURS_BEFORE (D-5 = 120h) vote_open_hours_before: int = 120 - # 마감 = 킥오프 1시간 전 (경기 시작 1시간 전까지 투표). lock 오프셋(분). - vote_lock_minutes_before: int = 60 + # 마감 = 킥오프 5분 전 (경기 시작 5분 전까지 투표). lock 오프셋(분). + vote_lock_minutes_before: int = 5 # 데모: True 면 오픈 게이트 무시(항상 투표 가능). 운영 배포 시 False. demo_force_open: bool = True diff --git a/frontend/src/components/Arena.tsx b/frontend/src/components/Arena.tsx index 167f6ee..bd749b5 100644 --- a/frontend/src/components/Arena.tsx +++ b/frontend/src/components/Arena.tsx @@ -9,7 +9,7 @@ import type { AIPrediction, Team, } from "@/lib/types"; -import { ApiError, getDeviceId, submitPrediction } from "@/lib/api"; +import { ApiError, getDeviceId, getRecentEmails, rememberEmail, submitPrediction } from "@/lib/api"; import Countdown from "./Countdown"; import TeamFlag from "./TeamFlag"; @@ -43,7 +43,8 @@ export default function Arena({ const [scoreA, setScoreA] = useState(0); const [scoreB, setScoreB] = useState(0); const [step, setStep] = useState("pick"); - const [email, setEmail] = useState(""); + const [recentEmails, setRecentEmails] = useState(getRecentEmails); + const [email, setEmail] = useState(() => recentEmails[0] ?? ""); const [notify, setNotify] = useState(true); const [crowd, setCrowd] = useState(initialCrowd); const [copied, setCopied] = useState(false); @@ -98,6 +99,8 @@ export default function Arena({ notify, }); setCrowd(res.crowd); // 서버 권위 분포로 갱신 + rememberEmail(email.trim()); // 다음 투표 자동완성용으로 기억 + setRecentEmails(getRecentEmails()); setStep("done"); } catch (e) { const msg = @@ -299,11 +302,20 @@ export default function Arena({ setEmail(e.target.value)} placeholder={t.emailPh} className="w-full rounded-lg border border-[var(--line-d)] bg-[#0f1217] px-3 py-3 text-[15px] text-white outline-none focus:border-[var(--green)]" /> + {recentEmails.length > 0 && ( + + {recentEmails.map((e) => ( + + )}