From b047c589d8092bb62e018b88911af37a71d79161 Mon Sep 17 00:00:00 2001 From: th-kim0823 Date: Sat, 25 Apr 2026 19:52:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8B=9C=EC=83=81=EC=8B=9D=20reveal=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20(=3Fmode=3Dceremony)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 단계 진행: 1. 시상식 시작 화면 → 시작 버튼 2. 부문별 announce (label + 상품) → 🥁🥁🥁 → 우승팀 공개 버튼 3. 우승팀 reveal: gradient gold 큰 폰트 + balloons + fadeIn 애니메이션 4. 다음 부문 → 반복 → 모든 시상 완료 (snow + balloons) 진행자 클릭만으로 진행. session_state로 단계 관리. CATEGORIES에 상품 매핑 (재미상=손선풍기, 완성도상=팜레스트, 실용성상=양우산). Co-Authored-By: Claude Opus 4.7 (1M context) --- app.py | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 107 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index b087dff..bd4a0c1 100644 --- a/app.py +++ b/app.py @@ -32,9 +32,9 @@ TEAMS = sorted(set(PARTICIPANTS.values())) if PARTICIPANTS else [ ] CATEGORIES = [ - ("fun_team", "🎉 재미상"), - ("polish_team", "🏆 완성도상"), - ("utility_team", "🛠 실용성상"), + ("fun_team", "🎉 재미상", "손선풍기 5개"), + ("polish_team", "🏆 완성도상", "팜레스트 5개"), + ("utility_team", "🛠 실용성상", "양우산 5개"), ] @@ -117,7 +117,7 @@ def render_voter(): with st.form("vote", clear_on_submit=False): st.divider() picks = {} - for col, label in CATEGORIES: + for col, label, _ in CATEGORIES: picks[col] = st.radio( label, candidates, @@ -128,7 +128,7 @@ def render_voter(): submitted = st.form_submit_button("제출") if submitted: - if any(picks.get(col) is None for col, _ in CATEGORIES): + if any(picks.get(col) is None for col, _, _ in CATEGORIES): st.error("3분야 모두 선택하세요.") return @@ -208,7 +208,7 @@ def render_admin(): public_lines = [] # 시상식 발표용 (하위 비공개) - for col, label in CATEGORIES: + for col, label, _ in CATEGORIES: rows = conn.execute( f"SELECT {col} AS team, COUNT(*) AS c FROM votes GROUP BY {col} ORDER BY c DESC, team ASC" ).fetchall() @@ -248,11 +248,111 @@ def render_admin(): conn.close() +def render_ceremony(): + """시상식 reveal 페이지. 진행자가 클릭으로 단계별 공개.""" + token = st.query_params.get("token", "") + if token != ADMIN_TOKEN: + st.error("권한 없음. ?mode=ceremony&token=... 형식 필요.") + return + + titles = get_titles() + conn = get_conn() + + results = [] + for col, label, prize in CATEGORIES: + rows = conn.execute( + f"SELECT {col} AS team, COUNT(*) AS c FROM votes " + f"GROUP BY {col} ORDER BY c DESC, team ASC" + ).fetchall() + if rows: + winner, votes = rows[0] + runner = rows[1][1] if len(rows) > 1 else 0 + results.append((label, prize, winner, votes, votes - runner)) + conn.close() + + if "ceremony_step" not in st.session_state: + st.session_state.ceremony_step = 0 + if "ceremony_revealed" not in st.session_state: + st.session_state.ceremony_revealed = False + + st.markdown( + """ + + """, + unsafe_allow_html=True, + ) + + step = st.session_state.ceremony_step + + if step == 0: + st.markdown('
🎉 해커톤 시상식 🎉
', unsafe_allow_html=True) + st.markdown('
준비됐습니다
', unsafe_allow_html=True) + if st.button("시작 →", use_container_width=True, type="primary"): + st.session_state.ceremony_step = 1 + st.session_state.ceremony_revealed = False + st.rerun() + + elif step > len(results): + st.markdown('
🎊 모든 시상 완료 🎊
', unsafe_allow_html=True) + st.balloons() + st.snow() + if st.button("처음으로", use_container_width=True): + st.session_state.ceremony_step = 0 + st.session_state.ceremony_revealed = False + st.rerun() + + else: + label, prize, winner, votes, diff = results[step - 1] + st.markdown(f'
{label}
', unsafe_allow_html=True) + st.markdown(f'
🎁 상품: {prize}
', unsafe_allow_html=True) + + if not st.session_state.ceremony_revealed: + st.markdown('
🥁🥁🥁
', unsafe_allow_html=True) + if st.button("우승팀 공개 →", use_container_width=True, type="primary"): + st.session_state.ceremony_revealed = True + st.rerun() + else: + st.balloons() + winner_label = fmt_team(winner, titles) + st.markdown( + f'
🏆
{winner_label}
', + unsafe_allow_html=True, + ) + st.markdown( + f'
{votes}표 (2위와 {diff}표 차이)
', + unsafe_allow_html=True, + ) + next_label = "다음 부문 →" if step < len(results) else "마무리 →" + if st.button(next_label, use_container_width=True, type="primary"): + st.session_state.ceremony_step = step + 1 + st.session_state.ceremony_revealed = False + st.rerun() + + def main(): - st.set_page_config(page_title="해커톤 투표", page_icon="🗳") + st.set_page_config(page_title="해커톤 투표", page_icon="🗳", layout="wide") mode = st.query_params.get("mode", "vote") if mode == "admin": render_admin() + elif mode == "ceremony": + render_ceremony() else: render_voter()