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:
114
app.py
114
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(
|
||||
"""
|
||||
<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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user