diff --git a/app.py b/app.py index 248deb4..172cf9e 100644 --- a/app.py +++ b/app.py @@ -49,6 +49,7 @@ def get_conn(): CREATE TABLE IF NOT EXISTS votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, voter_name TEXT NOT NULL UNIQUE, + employee_id TEXT, voter_team TEXT NOT NULL, fun_team TEXT NOT NULL, polish_team TEXT NOT NULL, @@ -57,6 +58,10 @@ def get_conn(): ) """ ) + # 기존 DB 마이그레이션 + cols = [r[1] for r in conn.execute("PRAGMA table_info(votes)").fetchall()] + if "employee_id" not in cols: + conn.execute("ALTER TABLE votes ADD COLUMN employee_id TEXT") conn.execute( """ CREATE TABLE IF NOT EXISTS team_titles ( @@ -131,9 +136,11 @@ def render_voter(): st.markdown( """ **투표 방식** -- 본인 이름 선택 → 본인 팀 자동 매핑 +- 본인 이름 선택 + **본인 사번 입력** +- 본인 팀 자동 매핑 - 본인 팀 **제외**, 다른 6팀 중 각 분야별 1팀씩 투표 (총 3표) - **한 번만 제출 가능** (이름 unique) +- 사번은 사칭 의심 시 추적용으로 기록됨 (본인 이름이 아닌데 투표한 경우 사번 주인에게 확인) **수상 결정 — 1팀 1상 한정** - 우선순위: 🛠 **실용성상 (팜레스트)** > 🏆 **완성도상 (양우산)** > 🎉 **재미상 (손선풍기)** @@ -158,10 +165,17 @@ def render_voter(): index=None, placeholder="이름 선택", ) + employee_id = st.text_input( + "사번", + placeholder="본인 사번 입력 (사칭 추적용으로 기록됨)", + ) if not name: st.info("이름을 선택하면 본인 팀이 자동 표시됩니다.") return + if not employee_id.strip(): + st.warning("사번 입력 후 진행하세요.") + return my_team = PARTICIPANTS[name] titles = get_titles() @@ -191,11 +205,12 @@ def render_voter(): conn.execute( """ INSERT INTO votes - (voter_name, voter_team, fun_team, polish_team, utility_team, created_at) - VALUES (?, ?, ?, ?, ?, ?) + (voter_name, employee_id, voter_team, fun_team, polish_team, utility_team, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) """, ( name, + employee_id.strip(), my_team, picks["fun_team"], picks["polish_team"], @@ -300,6 +315,50 @@ def render_admin(): st.subheader("🎤 시상식 발표용 (복사해서 화면 공유)") st.code("\n".join(public_lines), language="markdown") + st.divider() + with st.expander("🔍 감사 로그 (사칭 추적용)"): + st.caption("이름, 사번, 시각, 투표 내역. 의심 시 사번 주인에게 확인.") + rows = conn.execute( + "SELECT created_at, voter_name, employee_id, voter_team, " + "fun_team, polish_team, utility_team FROM votes ORDER BY created_at" + ).fetchall() + if not rows: + st.caption("투표 없음") + else: + import io + import csv + buf = io.StringIO() + w = csv.writer(buf) + w.writerow(["시각", "이름", "사번", "본인팀", "재미", "완성도", "실용성"]) + for r in rows: + w.writerow(r) + st.download_button("CSV 내려받기", buf.getvalue(), file_name="audit.csv") + st.dataframe( + { + "시각": [r[0] for r in rows], + "이름": [r[1] for r in rows], + "사번": [r[2] or "" for r in rows], + "본인팀": [r[3] for r in rows], + "재미": [r[4] for r in rows], + "완성도": [r[5] for r in rows], + "실용성": [r[6] for r in rows], + }, + use_container_width=True, + hide_index=True, + ) + + # 사번 중복 의심 (같은 사번이 여러 이름으로 투표) + from collections import defaultdict + by_emp = defaultdict(list) + for r in rows: + if r[2]: + by_emp[r[2]].append(r[1]) + dups = {emp: names for emp, names in by_emp.items() if len(set(names)) > 1} + if dups: + st.error("⚠️ 같은 사번이 여러 이름으로 투표한 케이스:") + for emp, names in dups.items(): + st.write(f"- 사번 `{emp}`: {', '.join(names)}") + st.divider() with st.expander("⚠️ 위험 작업"): if st.button("모든 투표 삭제 (되돌릴 수 없음)"):