diff --git a/app.py b/app.py index 172cf9e..4472426 100644 --- a/app.py +++ b/app.py @@ -62,6 +62,16 @@ def get_conn(): cols = [r[1] for r in conn.execute("PRAGMA table_info(votes)").fetchall()] if "employee_id" not in cols: conn.execute("ALTER TABLE votes ADD COLUMN employee_id TEXT") + conn.execute( + """ + CREATE TABLE IF NOT EXISTS tie_breaks ( + category TEXT PRIMARY KEY, + winner_team TEXT NOT NULL, + method TEXT NOT NULL, -- 'random' or 'manual' + decided_at TEXT NOT NULL + ) + """ + ) conn.execute( """ CREATE TABLE IF NOT EXISTS team_titles ( @@ -99,11 +109,48 @@ def fmt_team(team, titles): return f"{team} — {t}" if t else team +def get_tie_breaks(): + conn = get_conn() + rows = conn.execute("SELECT category, winner_team FROM tie_breaks").fetchall() + conn.close() + return dict(rows) + + +def save_tie_break(category, winner, method): + conn = get_conn() + conn.execute( + "INSERT INTO tie_breaks (category, winner_team, method, decided_at) " + "VALUES (?, ?, ?, ?) " + "ON CONFLICT(category) DO UPDATE SET " + "winner_team = excluded.winner_team, " + "method = excluded.method, " + "decided_at = excluded.decided_at", + (category, winner, method, datetime.now().isoformat(timespec="seconds")), + ) + conn.commit() + conn.close() + + +def clear_tie_break(category): + conn = get_conn() + conn.execute("DELETE FROM tie_breaks WHERE category = ?", (category,)) + conn.commit() + conn.close() + + def compute_winners(): """ - 우선순위 기반 1팀 1상 보장. - PRIZE_PRIORITY 순으로 결정, 이미 수상한 팀은 후순위 상에서 제외. - return: dict[col] = (winner_team, winner_votes, diff_with_2nd, all_rows_excluded) + 우선순위 기반 1팀 1상 + 동률 처리. + 동률 발견 시 tie_breaks 저장 결정 사용. 미결정이면 status='tie' 반환. + return: (winners dict, rankings dict) + winners[col] = { + "status": "ok"|"tie"|"empty", + "team": str, # ok 시 + "votes": int, + "diff": int, # ok 시 2위와 차이 + "tied": [str], # tie 시 동률 후보들 + "method": str|None, # tie_break 적용 방식 + } """ conn = get_conn() rankings = {} @@ -112,6 +159,11 @@ def compute_winners(): f"SELECT {col} AS team, COUNT(*) AS c FROM votes " f"GROUP BY {col} ORDER BY c DESC, team ASC" ).fetchall() + tie_decisions = dict( + conn.execute( + "SELECT category, winner_team || '||' || method FROM tie_breaks" + ).fetchall() + ) conn.close() winners = {} @@ -120,12 +172,38 @@ def compute_winners(): rows = rankings[col] filtered = [(t, c) for t, c in rows if t not in excluded] if not filtered: - winners[col] = None + winners[col] = {"status": "empty"} continue - winner, votes = filtered[0] - runner = filtered[1][1] if len(filtered) > 1 else 0 - winners[col] = (winner, votes, votes - runner, filtered) + + top_votes = filtered[0][1] + tied = [t for t, c in filtered if c == top_votes] + + if len(tied) > 1: + decision = tie_decisions.get(col, "") + chosen, _, method = decision.partition("||") + if chosen and chosen in tied: + winner = chosen + else: + winners[col] = { + "status": "tie", + "tied": tied, + "votes": top_votes, + } + continue + else: + winner = filtered[0][0] + method = "" + + runner_votes = next((c for t, c in filtered if t != winner), 0) + winners[col] = { + "status": "ok", + "team": winner, + "votes": top_votes, + "diff": top_votes - runner_votes, + "method": method, + } excluded.add(winner) + return winners, rankings @@ -280,9 +358,22 @@ def render_admin(): ) winners, rankings = compute_winners() - awarded_teams = {w[0] for w in winners.values() if w} + awarded_teams = { + w["team"] for w in winners.values() if w.get("status") == "ok" + } public_lines = [] + # 동률 미해결 부문 먼저 표시 (진행자 액션 필요) + pending_ties = [ + (col, label, prize) + for col, label, prize in CATEGORIES + if winners.get(col, {}).get("status") == "tie" + ] + if pending_ties: + st.error( + f"⚠️ 동률 미해결 {len(pending_ties)}건. 시상 전 추첨/선택 필요." + ) + for col, label, prize in CATEGORIES: rows = rankings[col] st.markdown(f"### {label} ({prize})") @@ -290,18 +381,61 @@ def render_admin(): st.caption("표 없음") continue - result = winners.get(col) - if not result: + result = winners.get(col, {}) + status = result.get("status") + + if status == "empty": st.warning("후보 없음 (모두 우선순위 상 수상)") continue - winner_team, winner_votes, diff, _ = result + if status == "tie": + tied = result["tied"] + votes = result["votes"] + tied_labels = ", ".join(fmt_team(t, titles) for t in tied) + st.warning(f"🟰 동률 ({votes}표): {tied_labels}") + ca, cb = st.columns([1, 2]) + with ca: + if st.button("🎲 즉석 추첨", key=f"draw_{col}"): + import random as _r + chosen = _r.choice(tied) + save_tie_break(col, chosen, "random") + st.rerun() + with cb: + manual = st.selectbox( + "수동 선택", + tied, + index=None, + key=f"manual_{col}", + format_func=lambda t: fmt_team(t, titles), + placeholder="팀 선택", + ) + if manual and st.button("확정", key=f"manual_btn_{col}"): + save_tie_break(col, manual, "manual") + st.rerun() + continue + + # status == "ok" + winner_team = result["team"] + winner_votes = result["votes"] + diff = result["diff"] + method = result.get("method", "") winner_label = fmt_team(winner_team, titles) + method_tag = "" + if method == "random": + method_tag = " 🎲(추첨)" + elif method == "manual": + method_tag = " 🖊️(수동)" st.success( - f"**우승: {winner_label}** — {winner_votes}표 (2위와 {diff}표 차이)" + f"**우승: {winner_label}**{method_tag} — {winner_votes}표 " + f"(2위와 {diff}표 차이)" ) + if method: + if st.button(f"동률 결정 취소 (재추첨)", key=f"clear_{col}"): + clear_tie_break(col) + st.rerun() public_lines.append( - f"- {label} 우승: **{winner_label}** ({winner_votes}표, 2위와 {diff}표 차이)" + f"- {label} 우승: **{winner_label}** " + f"({winner_votes}표, 2위와 {diff}표 차이)" ) with st.expander("전체 분포 — raw (제외 적용 전)"): @@ -379,13 +513,27 @@ def render_ceremony(): titles = get_titles() winners, _ = compute_winners() + # 동률 미해결 있으면 ceremony 차단 + pending = [ + (col, label) for col, label, _ in CATEGORIES + if winners.get(col, {}).get("status") == "tie" + ] + if pending: + st.error("⚠️ 동률 미해결 — 어드민 페이지에서 추첨/선택 후 재진입") + for col, label in pending: + tied = winners[col]["tied"] + tied_labels = ", ".join(fmt_team(t, titles) for t in tied) + st.write(f"- {label}: {tied_labels}") + return + # CATEGORIES 순서로 reveal (손선풍기 → 양우산 → 팜레스트) results = [] for col, label, prize in CATEGORIES: - result = winners.get(col) - if result: - winner, votes, diff, _ = result - results.append((label, prize, winner, votes, diff)) + result = winners.get(col, {}) + if result.get("status") == "ok": + results.append( + (label, prize, result["team"], result["votes"], result["diff"]) + ) if "ceremony_step" not in st.session_state: st.session_state.ceremony_step = 0