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>
This commit is contained in:
th-kim0823
2026-04-27 19:26:07 +09:00
parent 067e25116a
commit 54e28e6f1e
2 changed files with 293 additions and 0 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ venv/
.env
!.env.example
.streamlit/secrets.toml
.superpowers/

View File

@@ -0,0 +1,292 @@
# 해커톤 진행 앱 — 전체 흐름 (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가 시드)