feat: 엣지 케이스 5개 처리
1. 투표 마감 락 (settings 테이블 + 어드민 토글) - 시상 도중 결과 바뀜 방지 - ceremony 진입 시 voting_open이면 경고 + 차단 2. 빈 결과 ceremony 차단 - 투표 0건이면 진입 불가 3. 타임존 KST (Dockerfile tzdata + TZ=Asia/Seoul) - 감사 로그 시각 정확 4. CSV UTF-8 BOM - Excel에서 한글 정상 표시 5. 사번 입력 안내 강화 - placeholder + help: 민감정보 입력 금지 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,12 @@ FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 한국 시간대 (감사 로그 정확성)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends tzdata && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ENV TZ=Asia/Seoul
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
|
||||
77
app.py
77
app.py
@@ -72,6 +72,14 @@ def get_conn():
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS team_titles (
|
||||
@@ -109,6 +117,26 @@ def fmt_team(team, titles):
|
||||
return f"{team} — {t}" if t else team
|
||||
|
||||
|
||||
def is_voting_open():
|
||||
conn = get_conn()
|
||||
row = conn.execute(
|
||||
"SELECT value FROM settings WHERE key = 'voting_open'"
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return row is None or row[0] == "1"
|
||||
|
||||
|
||||
def set_voting_open(flag):
|
||||
conn = get_conn()
|
||||
conn.execute(
|
||||
"INSERT INTO settings (key, value) VALUES ('voting_open', ?) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
("1" if flag else "0",),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_tie_breaks():
|
||||
conn = get_conn()
|
||||
rows = conn.execute("SELECT category, winner_team FROM tie_breaks").fetchall()
|
||||
@@ -210,6 +238,10 @@ def compute_winners():
|
||||
def render_voter():
|
||||
st.title("🗳 해커톤 투표")
|
||||
|
||||
if not is_voting_open():
|
||||
st.error("⏹️ 투표가 마감되었습니다. 시상식을 기다려주세요.")
|
||||
return
|
||||
|
||||
with st.expander("📖 투표 방식 / 수상 결정 (꼭 읽어주세요)", expanded=True):
|
||||
st.markdown(
|
||||
"""
|
||||
@@ -245,7 +277,8 @@ def render_voter():
|
||||
)
|
||||
employee_id = st.text_input(
|
||||
"사번",
|
||||
placeholder="본인 사번 입력 (사칭 추적용으로 기록됨)",
|
||||
placeholder="본인 사번만 (전화번호/주민번호 입력 금지)",
|
||||
help="사칭 추적용으로 기록됨. 본인 사번 외 입력하지 마세요.",
|
||||
)
|
||||
|
||||
if not name:
|
||||
@@ -313,6 +346,22 @@ def render_admin():
|
||||
|
||||
st.title("🔐 진행자 콘솔")
|
||||
|
||||
# 투표 마감 토글
|
||||
voting_open = is_voting_open()
|
||||
cur_label = "🟢 투표 진행 중" if voting_open else "🔴 투표 마감됨"
|
||||
st.markdown(f"### 투표 상태: {cur_label}")
|
||||
cc1, cc2 = st.columns(2)
|
||||
with cc1:
|
||||
if voting_open and st.button("🛑 투표 마감 (시상 시작 전 필수)", type="primary"):
|
||||
set_voting_open(False)
|
||||
st.rerun()
|
||||
with cc2:
|
||||
if not voting_open and st.button("🔓 투표 재개"):
|
||||
set_voting_open(True)
|
||||
st.rerun()
|
||||
|
||||
st.divider()
|
||||
|
||||
conn = get_conn()
|
||||
total = conn.execute("SELECT COUNT(*) FROM votes").fetchone()[0]
|
||||
|
||||
@@ -466,7 +515,14 @@ def render_admin():
|
||||
w.writerow(["시각", "이름", "사번", "본인팀", "재미", "완성도", "실용성"])
|
||||
for r in rows:
|
||||
w.writerow(r)
|
||||
st.download_button("CSV 내려받기", buf.getvalue(), file_name="audit.csv")
|
||||
# UTF-8 BOM → Excel 한글 호환
|
||||
csv_data = "" + buf.getvalue()
|
||||
st.download_button(
|
||||
"CSV 내려받기",
|
||||
csv_data,
|
||||
file_name="audit.csv",
|
||||
mime="text/csv; charset=utf-8",
|
||||
)
|
||||
st.dataframe(
|
||||
{
|
||||
"시각": [r[0] for r in rows],
|
||||
@@ -511,6 +567,23 @@ def render_ceremony():
|
||||
return
|
||||
|
||||
titles = get_titles()
|
||||
|
||||
# 투표 0건이면 ceremony 진입 불가
|
||||
conn = get_conn()
|
||||
total_votes = conn.execute("SELECT COUNT(*) FROM votes").fetchone()[0]
|
||||
conn.close()
|
||||
if total_votes == 0:
|
||||
st.warning("📊 아직 투표가 없습니다. 투표 종료 후 시상식을 시작하세요.")
|
||||
return
|
||||
|
||||
# 투표가 아직 열려 있으면 경고 (시상 도중 결과 변경 위험)
|
||||
if is_voting_open():
|
||||
st.error(
|
||||
"⚠️ 투표가 아직 진행 중입니다. 어드민에서 '투표 마감' 후 시상하세요. "
|
||||
"(현재 결과가 시상 중 바뀔 수 있음)"
|
||||
)
|
||||
return
|
||||
|
||||
winners, _ = compute_winners()
|
||||
|
||||
# 동률 미해결 있으면 ceremony 차단 (부문/후보 정보는 노출 X — 발표 spoiler 방지)
|
||||
|
||||
Reference in New Issue
Block a user