feat: 시상식 reveal 페이지 (?mode=ceremony)

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) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-04-25 19:52:32 +09:00
parent 0c73d655a7
commit b047c589d8

114
app.py
View File

@@ -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(
"""
<style>
.stage-title { font-size: 70px; text-align: center; padding: 30px 0; }
.stage-prize { font-size: 32px; text-align: center; color: #888; }
.stage-drum { font-size: 90px; text-align: center; padding: 60px 0; }
.winner-name {
font-size: 110px; text-align: center; font-weight: bold;
background: linear-gradient(90deg, #ffd700, #ff8c00);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
padding: 40px 0;
}
.winner-meta { font-size: 36px; text-align: center; padding: 20px 0; }
.stReveal { animation: fadeIn 1.5s ease-in; }
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.5); }
to { opacity: 1; transform: scale(1); }
}
</style>
""",
unsafe_allow_html=True,
)
step = st.session_state.ceremony_step
if step == 0:
st.markdown('<div class="stage-title">🎉 해커톤 시상식 🎉</div>', unsafe_allow_html=True)
st.markdown('<div class="winner-meta">준비됐습니다</div>', 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('<div class="stage-title">🎊 모든 시상 완료 🎊</div>', 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'<div class="stage-title">{label}</div>', unsafe_allow_html=True)
st.markdown(f'<div class="stage-prize">🎁 상품: {prize}</div>', unsafe_allow_html=True)
if not st.session_state.ceremony_revealed:
st.markdown('<div class="stage-drum">🥁🥁🥁</div>', 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'<div class="winner-name stReveal">🏆<br>{winner_label}</div>',
unsafe_allow_html=True,
)
st.markdown(
f'<div class="winner-meta">{votes}표 (2위와 {diff}표 차이)</div>',
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()