Files
hackerthon-vote/docs/superpowers/specs/2026-04-27-hackathon-event-flow-design.md
th-kim0823 54e28e6f1e docs: spec — 해커톤 진행 앱 (intro → topics → vote → ceremony)
Stage 기반 큰 화면 흐름 + 모바일 QR 투표. 어드민에서 stage 컨트롤
+ 주제 4 카테고리 × 10 편집. 기존 voter/admin/ceremony는 그대로.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:26:07 +09:00

293 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 해커톤 진행 앱 — 전체 흐름 (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가 시드)