From 54e28e6f1ee191f5b1b69a1e68197fee6211e2ff Mon Sep 17 00:00:00 2001 From: th-kim0823 Date: Mon, 27 Apr 2026 19:26:07 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20spec=20=E2=80=94=20=ED=95=B4=EC=BB=A4?= =?UTF-8?q?=ED=86=A4=20=EC=A7=84=ED=96=89=20=EC=95=B1=20(intro=20=E2=86=92?= =?UTF-8?q?=20topics=20=E2=86=92=20vote=20=E2=86=92=20ceremony)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 기반 큰 화면 흐름 + 모바일 QR 투표. 어드민에서 stage 컨트롤 + 주제 4 카테고리 × 10 편집. 기존 voter/admin/ceremony는 그대로. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + .../2026-04-27-hackathon-event-flow-design.md | 292 ++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-27-hackathon-event-flow-design.md diff --git a/.gitignore b/.gitignore index 8243d69..dd69be1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ venv/ .env !.env.example .streamlit/secrets.toml +.superpowers/ diff --git a/docs/superpowers/specs/2026-04-27-hackathon-event-flow-design.md b/docs/superpowers/specs/2026-04-27-hackathon-event-flow-design.md new file mode 100644 index 0000000..6cc3d5a --- /dev/null +++ b/docs/superpowers/specs/2026-04-27-hackathon-event-flow-design.md @@ -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가 시드)