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

13 KiB
Raw Permalink Blame History

해커톤 진행 앱 — 전체 흐름 (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.jsontopics + 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

{
  "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:

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.shassign_teams.py 호출. 그 안 마지막에 ensure_topics_seeded() 호출 추가:

# assign_teams.py 끝 부분
def ensure_topics_seeded(data):
    if not data.get("topics", {}).get("categories"):
        data["topics"] = DEFAULT_TOPICS_SEED

DEFAULT_TOPICS_SEEDassign_teams.py에 모듈 상수로 박제 (위 4 카테고리 × 10 주제).

테스트 (tests/e2e.py)

기존 12 + 신규 4 → 총 16 시나리오:

  1. stage 시드 — fresh hackathon.jsoncurrent_stage="intro" + topics 4 카테고리 각 10 items.
  2. stage 전환 + voting_open 자동set_stage("vote")voting_open == True. set_stage("intro")voting_open 변경 없음 (이전 상태 유지).
  3. stage gatecan_accept_votes(data)가 stage/voting_open 조합 4가지에서 정확한 bool 리턴. (intro+open=False, vote+open=True, vote+closed=False, topics+open=False)
  4. topics atomic updateupdate_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.pyDEFAULT_TOPICS_SEED + ensure_topics_seeded() + 호출.
  • requirements.txtqrcode[pil], streamlit-autorefresh 추가.
  • tests/e2e.py — 신규 4 시나리오.
  • show-urls.sh — show / vote URL 추가.
  • README.md — 새 흐름 설명.
  • (hackathon.json 자체는 entrypoint가 시드)