From 955ffae4cfec8f570f4c6a91b7266bb55385f898 Mon Sep 17 00:00:00 2001 From: hbyang Date: Fri, 12 Jun 2026 16:43:28 +0900 Subject: [PATCH] Open voting for all groups by D-2 time + 30h-ahead AI gen; misc fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - domain: is_votable → all groups (voting gated by D-2 open ~ kickoff-60m time window, not group) - worker: AI generation selects matches by opens_at <= now+lookahead (all groups), pre-fills before D-2 open so picks aren't empty; only_missing keeps total calls = 3/match - config: ai_generate_lookahead_hours=30 (covers daily-only gen cadence) - Arena: default pick 0:0 (무승부) - frontend Dockerfile: install from package.json only (macOS-generated lock leaves rollup darwin-x64 stub w/o version → npm "Invalid Version" on linux) NOTE for deploy: set DEMO_FORCE_OPEN=false and real PUBLIC_ORIGIN in server .env. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/config.py | 3 +++ backend/app/domain.py | 5 +++-- backend/app/worker.py | 13 +++++++------ frontend/Dockerfile | 8 +++++--- frontend/src/components/Arena.tsx | 6 +++--- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index 58cd980..bab425a 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -84,6 +84,9 @@ class Settings(BaseSettings): # 매일 AI 예측 생성 시각 (KST). 기능정의서: 매일 0시 1회 생성. ai_generate_hour: int = 0 ai_generate_minute: int = 5 + # 투표 오픈(D-2) 선행 생성 시간(h). 1일 1회 생성이므로 다음 주기 전 오픈할 + # 경기를 미리 채우려면 ~24h 이상 필요. 30h = 하루치 + 여유. (only_missing 이라 총 호출 수 동일) + ai_generate_lookahead_hours: int = 30 # ── 결과 자동 정산(관리자 입력 불필요) ─────────────────── # 킥오프 + 이 시간(시) 경과 후, 외부 소스에서 스코어를 자동 수집해 채점·집계·메일. diff --git a/backend/app/domain.py b/backend/app/domain.py index d551c9d..3b8dacc 100644 --- a/backend/app/domain.py +++ b/backend/app/domain.py @@ -59,8 +59,9 @@ def compute_phase(m: Match, now: datetime | None = None) -> str: def is_votable(m: Match) -> bool: - """AI 예측·투표 대상 조인지. featured_group 만 투표 가능, 그 외는 일정/결과만.""" - return m.group == settings.featured_group + """투표 지원 경기 여부 — 전체 조 투표 가능. + 실제 오픈/마감은 is_open_for_voting 의 시간창(D-2 오픈 ~ 킥오프 60분 전 마감)이 결정.""" + return True def is_open_for_voting(m: Match, now: datetime | None = None) -> bool: diff --git a/backend/app/worker.py b/backend/app/worker.py index 5f43c3a..773125b 100644 --- a/backend/app/worker.py +++ b/backend/app/worker.py @@ -83,17 +83,18 @@ async def generate_ai_predictions(only_missing: bool = True) -> None: only_missing=False: 전부 재생성(덮어쓰기) — 수동 재생성 스크립트용. """ async with SessionLocal() as db: - # AI 예측·투표 대상 조(featured_group)만 생성. 그 외 조는 일정/결과만. - matches = ( + # 투표 오픈(킥오프 D-2)이 임박/도래한 미종료 경기 — 조 무관. + # lookahead_hours 만큼 선행 생성해 다음 생성 주기 전에 오픈할 경기도 미리 채운다 + # (1일 1회 생성 → 오픈 시점에 AI 가 비어 보이지 않도록). 더 먼 미래 경기는 제외. + horizon = now_utc() + timedelta(hours=settings.ai_generate_lookahead_hours) + rows = ( await db.execute( select(Match) - .where( - Match.result_outcome.is_(None), - Match.group == settings.featured_group, - ) + .where(Match.result_outcome.is_(None)) .options(selectinload(Match.predictions)) ) ).scalars().all() + matches = [m for m in rows if ensure_aware(m.opens_at) <= horizon] for m in matches: ctx = MatchContext( diff --git a/frontend/Dockerfile b/frontend/Dockerfile index d0ff5c8..fdcec4e 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,8 +1,10 @@ # TriplePick 프론트엔드 — Vite 빌드 → nginx 정적 서빙 + /api 프록시. -FROM node:20-alpine AS build +# lock은 macOS(arm64)에서 생성돼 rollup darwin-x64 optional dep가 version 없는 +# stub로 남는 npm 버그가 있어, linux 빌드에선 lock 없이 package.json만으로 설치한다. +FROM node:24-alpine AS build WORKDIR /app -COPY package.json package-lock.json* ./ -RUN npm install +COPY package.json ./ +RUN npm install --no-audit --no-fund COPY . . RUN npm run build diff --git a/frontend/src/components/Arena.tsx b/frontend/src/components/Arena.tsx index 8186b40..167f6ee 100644 --- a/frontend/src/components/Arena.tsx +++ b/frontend/src/components/Arena.tsx @@ -39,9 +39,9 @@ export default function Arena({ const t = dict(lang); const aShort = teamShort(match.teamA, lang); const bShort = teamShort(match.teamB, lang); - const [outcome, setOutcome] = useState(null); - const [scoreA, setScoreA] = useState(2); - const [scoreB, setScoreB] = useState(1); + const [outcome, setOutcome] = useState("DRAW"); + const [scoreA, setScoreA] = useState(0); + const [scoreB, setScoreB] = useState(0); const [step, setStep] = useState("pick"); const [email, setEmail] = useState(""); const [notify, setNotify] = useState(true);