# 해커톤 진행 앱 — 전체 흐름 (intro → topics → vote → ceremony) 날짜: 2026-04-27 대상 코드베이스: `/Users/user/projects/hackathon-vote` 변경 종류: 기능 확장 (기존 투표 앱 → 행사 진행 앱) ## 배경 기존 앱은 투표·시상 전용. 이번 변경으로 해커톤 시작부터 끝까지 한 앱이 진행한다. 추가되는 단계: - **Stage 1 — 팀 편성 + 안내**: 큰 화면에 7팀 + 해커톤 순서 + 시상 부문. - **Stage 2 — 주제 예시**: 4 카테고리 × 10 주제 (영감용). - **Stage 3 — 투표 (모바일 진입)**: 큰 화면에 QR + 진행률. 모바일이 QR scan 시 기존 voter UI. - **시상**: 기존 `/?mode=ceremony&token=...` 그대로. ## 스코프 ### 포함 - 큰 화면 default `/` 라우트가 `current_stage` 따라 dispatch (intro / topics / vote). - 어드민 콘솔에 stage 컨트롤 + topics 편집 추가. - `hackathon.json`에 `topics` + `settings.current_stage` 키 추가. - QR 코드 생성 (`qrcode[pil]`) + auto-refresh (`streamlit-autorefresh`). - 사용자 제공 4 카테고리 × 10 주제 시드. ### 제외 - ceremony 변경 (그대로). - 모바일 voter UI 재디자인 (기존 그대로 사용). - 인증/세션/권한 변경 (기존 token 그대로). - 다국어 / 테마 변경. ## 라우트 | URL | 화면 | 권한 | |---|---|---| | `/` | 큰 화면 stage 진행 (`current_stage` dispatcher) | open | | `/?mode=vote` | 모바일 투표 화면 (기존 voter UI) | open (단, stage gate 적용) | | `/?mode=admin&token=…` | 어드민 콘솔 (stage 컨트롤 + 주제 편집 + 기존) | token | | `/?mode=ceremony&token=…` | 시상 reveal (기존 그대로) | token | | `/?mode=raw&token=…` | JSON 원본 조회/다운로드 (기존) | token | 기존 `mode=admin/ceremony/raw`는 동작 변경 없음. ## Stage 모델 `hackathon.json.settings.current_stage` ∈ `{"intro", "topics", "vote"}` (default `"intro"`). 전환 책임은 어드민. 이전/다음 자유롭게 이동 가능 (intro↔topics↔vote 양방향). `vote`로 set 시 `voting_open=True` 자동 같이 set. `vote`에서 떠나도 `voting_open`은 그대로 유지 — 어드민의 별도 "투표 마감" 버튼이 명시적으로 `False` 세팅. 단, voter URL은 `current_stage == "vote"` 조건도 같이 보므로 stage가 vote 아니면 자동 차단됨 (gate 함수 참조). ceremony는 stage가 아님. 별도 URL로 진행자가 띄움. vote 마감 후 진입하는 게 자연스럽지만 강제 없음. ## 데이터 스키마 — `hackathon.json` ```json { "settings": { "voting_open": true, "current_stage": "intro" }, "topics": { "categories": [ { "id": "T1", "title": "내 인생 시간 도둑 처단기", "tagline": "매일 짜증나는 반복 작업 하나를 2시간 안에 박살내기.", "tone": "실용 + 살짝 치트키", "items": [ "Slack 멘션 자동 분류/요약기 — '진짜 날 부른 거' vs 'FYI' 분리", "Jira 티켓 1줄 자동 요약 + 다음 액션 제안기", "회의 캘린더 → 하루 시작 브리핑 (\"오늘 3개 있고 2개는 안 가도 됨\")", "PR 리뷰 우선순위 큐 (크기/긴급도/차단여부 기반)", "반복 쿼리/kubectl 명령 매크로 CLI — 자주 치는 10개를 1글자로", "온콜 노이즈 필터 — 진짜 볼 알람 vs 무시해도 되는 알람", "\"이번 주 내 활동 자동 요약\" — PR/티켓/리뷰 통합 리포트", "Grafana 자주 보는 패널 즐겨찾기 통합 뷰", "Slack 스레드 장문 요약기 — 놓친 채널 따라잡기용", "\"이 회의 들어가야 함?\" 분류기 — 캘린더 제목·참석자 기반 추천" ] }, { "id": "T2", "title": "벼르던 사이드 프로젝트", "tagline": "평소 \"저거 하나 만들고 싶은데\" 하던 개인 토이를 2시간 안에 작은 완성품으로.", "tone": "몰입 + 작은 완결성", "items": [ "내 PR/커밋 패턴 분석 개인 대시보드 (시간대/요일/사이즈 분포)", "팀 Wiki/Notion을 터미널에서 fzf 스타일로 검색하는 CLI", "사내 모델/데이터셋 메타데이터 검색 프로토타입", "git 히스토리 인터랙티브 시각화 뷰어", "\"오늘 내가 한 일\" 자동 일기 생성기 (커밋/PR/티켓 통합)", "PR 코멘트 감정/톤 분석으로 팀 리뷰 문화 리포트", "로컬 Kubernetes 리소스 관계 그래프 실시간 시각화", "사내 논문/테크 문서 RAG 검색 도구", "터미널에서 차트 포함된 마크다운 뷰어", "북마크/링크 자동 분류·태깅 개인 도구" ] }, { "id": "T3", "title": "오버엔지니어링 선수권", "tagline": "평소 안 써본 무거운 기술 패턴을 일부러 작은 문제에 적용해 배우기.", "tone": "학습형 오버엔지니어링", "items": [ "Todo 앱에 이벤트 소싱 + CQRS 제대로 적용", "간단한 계산기 서비스에 OpenTelemetry 풀 트레이싱 구축", "문서 검색 기능에 벡터 DB + 하이브리드 검색 (BM25 + 임베딩)", "파일 업로드에 S3 presigned URL + 체크섬 검증 + 재시도 로직 정식 설계", "팀 투표 기능을 Raft 합의 알고리즘으로 구현", "회의실 예약을 Kafka 이벤트 스트리밍 기반으로", "로컬 개발 환경을 완전한 K8s 매니페스트 (Deployment/Service/Ingress/HPA)로 재현", "LLM + RAG 기반 PR 자동 리뷰 봇 아키텍처 설계·구현", "멀티 에이전트 협업(프롬프트 2~3단계)으로 간단한 의사결정 시스템", "사이드카 패턴으로 로깅/메트릭/인증 분리 데모" ] }, { "id": "T4", "title": "팀에게 주는 작은 선물", "tagline": "동료를 돕는 도구/봇/사이트. 특정인을 놀리는 게 아니라 팀 전체를 위한 것.", "tone": "실질적 도움 + 가벼운 온기", "items": [ "배포 상태 집계·알림 봇 (성공/실패/롤백 요약)", "신입 한 주 서바이벌 가이드 자동 생성기 (온보딩 링크/문서 수집)", "팀 내부 용어/약어 사전 봇 — 신입/리서처 친화", "아침 브리핑 봇 — 오늘 회의/배포/만료 알람 한방", "점심 투표 1분 컷 봇 — 선택지 자동 생성 후 이모지 투표", "팀 반복 질문 FAQ 봇 — 같은 질문 반복되는 채널용", "온콜 교대 시 인수인계 자동 요약 생성기", "회의실 스마트 추천 — 인원/시간대/위치 기반", "사내 서비스 변경사항 요약 구독 봇", "\"이번 주 팀 지표 한 장\" 리포트 — 머지 PR, 해결 티켓, 배포 수" ] } ] }, "people": [...], "votes": [...], "titles": {...}, "tie_breaks": {...} } ``` 핫리로드: 매 요청 `load_data()` 그대로. 변경 즉시 반영. ## 컴포넌트 ### `app.py` 추가 함수 | 함수 | 역할 | |---|---| | `render_show()` | `/` 진입 시 dispatcher. `current_stage` 따라 stage 함수 호출. | | `render_stage_intro(data)` | 팀 편성 4×2 그리드 + 순서 + 시상 부문. 큰 글씨. | | `render_stage_topics(data)` | 4 카테고리 2×2 그리드 + 10 주제 list. 큰 글씨. | | `render_stage_vote(data)` | QR + n/total 진행률. autorefresh 3초. | | `set_stage(stage)` | `current_stage` + `voting_open` 갱신 (`vote`이면 `True`). atomic write. | | `get_stage()` | 현재 stage 리턴. | | `update_topics(categories)` | topics 통째 교체. atomic write. | | `seed_topics(data)` | topics 비어있으면 default 4 카테고리 박제. | | `make_qr_png(url)` | qrcode + PIL → PNG bytes. | | `compute_vote_url(data)` | public_base_url 우선순위로 vote URL 결정. | ### 어드민 콘솔 신규 section 기존 `render_admin()`에 추가: ``` 🎬 Stage 진행 현재: [intro | topics | vote] (radio 또는 시각 표시) [← 이전] [다음 →] 🗒 주제 편집 Tab 1 — Form (4 expander, 카테고리별 title/tagline/tone/items 10개 input) Tab 2 — JSON 직접 편집 (textarea + 검증 + 저장) ``` 추가로: - "현재 QR이 가리키는 URL" 표시. - "QR target URL 수동 override" 입력란 → `settings.public_base_url` 저장. ### Vote URL 결정 로직 우선순위: 1. `data["settings"].get("public_base_url")` (어드민 입력). 2. 환경변수 `PUBLIC_BASE_URL`. 3. 자동 감지 — 호스트 LAN IP (Python `socket.gethostbyname(socket.gethostname())` 또는 `/proc/net/route` 기반 첫 default-route NIC). 4. fallback `http://localhost:8501`. → 결과에 `?mode=vote` append. ### Stage gate (모바일 voter) 신규 helper: ```python def can_accept_votes(data) -> bool: s = data.get("settings", {}) return s.get("current_stage") == "vote" and s.get("voting_open", False) ``` `render_voter()` 진입 시 `can_accept_votes(load_data())` 체크: - False → "지금은 투표 시간이 아닙니다 ⏳" 메시지 표시 후 return. - True → 기존 흐름 (이름 select → 사번 → 3 picks → 제출). `insert_vote()`는 현행 그대로 (lock + duplicate check만). 진입 단계에서 가드. ### 큰 화면 CSS `render_show()`에서 한 번 inject. Stage별 컴포넌트가 사용: - `.stage-title { font-size: 70px; }` - `.stage-section-title { font-size: 40px; }` - `.team-card { font-size: 24px; padding: 20px; }` - `.topic-cat-T1 { background: linear-gradient(...주황...) }` (T1~T4 색상) - `.topic-item { font-size: 18px; }` - `.qr-caption { font-size: 32px; }` - `.vote-counter { font-size: 64px; font-weight: bold; }` ## 신규 의존성 `requirements.txt`에 2줄 추가: ``` qrcode[pil] streamlit-autorefresh ``` ## 시드 (entrypoint) 기존 `entrypoint.sh`가 `assign_teams.py` 호출. 그 안 마지막에 `ensure_topics_seeded()` 호출 추가: ```python # assign_teams.py 끝 부분 def ensure_topics_seeded(data): if not data.get("topics", {}).get("categories"): data["topics"] = DEFAULT_TOPICS_SEED ``` `DEFAULT_TOPICS_SEED`는 `assign_teams.py`에 모듈 상수로 박제 (위 4 카테고리 × 10 주제). ## 테스트 (`tests/e2e.py`) 기존 12 + 신규 4 → 총 16 시나리오: 1. **stage 시드** — fresh `hackathon.json`에 `current_stage="intro"` + topics 4 카테고리 각 10 items. 2. **stage 전환 + voting_open 자동** — `set_stage("vote")` 후 `voting_open == True`. `set_stage("intro")` 후 `voting_open` 변경 없음 (이전 상태 유지). 3. **stage gate** — `can_accept_votes(data)`가 stage/voting_open 조합 4가지에서 정확한 bool 리턴. (intro+open=False, vote+open=True, vote+closed=False, topics+open=False) 4. **topics atomic update** — `update_topics(new_categories)` 후 reload 시 동일. 추가 회귀 보호: - 기존 12개 그대로 통과해야 함. - 라우트 5개 (`/`, `/?mode=vote`, `/?mode=admin&token=…`, `/?mode=ceremony&token=…`, `/?mode=raw&token=…`) HTTP 200. ## 실패/엣지 케이스 | 상황 | 동작 | |---|---| | `topics` 키 없음 (구버전 JSON) | `_empty_state` 패턴으로 빈 dict 채움 + entrypoint에서 seed. | | `current_stage` 값 이상 (예: `"foobar"`) | dispatcher fallback `intro`. | | QR target URL 결정 실패 | fallback `http://localhost:8501/?mode=vote`. 어드민에 경고 표시. | | topics JSON 직접 편집에서 invalid JSON | "JSON 검증" 단계에서 reject + 에러 표시, 저장 안 함. | | autorefresh 패키지 미설치 | requirements 누락 → import 단계 실패. fallback 없음. | | 동시 stage 변경 (admin 두 명) | `_lock` + atomic write로 직렬화. 마지막 write 우선. | | 모바일에서 stage가 vote 아닌데 직접 URL 입력 | gate 메시지 표시. | ## 운영 흐름 (예상) 1. `docker compose up -d --build` (기존 그대로). 2. 진행자: `?mode=admin&token=mlops2026` 진입. Stage `intro` 확인. 3. 큰 화면: `/` 띄움 (stage `intro` — 팀 편성). 4. 본 행사 시작 → 진행자가 어드민 "다음 →" 클릭 → 큰 화면 `topics`. 5. 주제 둘러보고 팀별 해킹 진행 (앱 외부). 6. 발표 끝나고 진행자 "다음 →" 클릭 → 큰 화면 `vote` (QR + 진행률, `voting_open=True` 자동). 참가자 휴대폰 QR scan → 모바일 투표. 7. 모두 투표 → 어드민 "🛑 투표 마감". 8. 동률 있으면 어드민에서 추첨/선택. 팀별 제목 입력. 9. ceremony URL 띄움 → 시상. ## 비스코프 / 차후 - 주제 카테고리 수 가변 (4 고정). - 모바일 vote UI 재디자인. - WebSocket 푸시 (현재는 polling으로 충분). - 다국어 / 테마. - 시드 다국어 / 행사별 템플릿. ## 변경 파일 목록 요약 - `app.py` — 신규 함수 + admin section 확장. - `assign_teams.py` — `DEFAULT_TOPICS_SEED` + `ensure_topics_seeded()` + 호출. - `requirements.txt` — `qrcode[pil]`, `streamlit-autorefresh` 추가. - `tests/e2e.py` — 신규 4 시나리오. - `show-urls.sh` — show / vote URL 추가. - `README.md` — 새 흐름 설명. - (`hackathon.json` 자체는 entrypoint가 시드)