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()