Compare commits

...

10 Commits

Author SHA1 Message Date
th-kim0823
addde1a0ea chore: repo 정리 — entrypoint.sh 추가, runtime state gitignore
- entrypoint.sh: 첫 부팅 시 assign_teams.py 자동 실행 (시드)
- Dockerfile: ENTRYPOINT 적용, DATA_PATH=/app/data/hackathon.json
- hackathon.json (root) 삭제 — data/ 디렉터리로 이전 (gitignore)
- teams.md 추적 해제 (assign_teams.py가 매번 재생성)
- results_*.json + data/ gitignore 추가
- .env.example 삭제 (compose에 ADMIN_TOKEN 박제, .env 불필요)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:31:29 +09:00
th-kim0823
447f067ae9 fix: admin stage 진행 — radio + 적용 버튼 제거 (session_state mismatch 버그)
이전/다음 두 버튼만 유지. radio key 고정으로 stage 변경 후
session_state가 stale → 적용 버튼이 의도치 않게 뜨던 문제 제거.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:23:10 +09:00
th-kim0823
75aa0349ff fix: admin UX — QR placeholder, 투표 상태 분리, 제목 placeholder 다양화, 시상 빈블럭 가드
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 21:18:48 +09:00
th-kim0823
e872b841e3 feat: PUBLIC_BASE_URL default = https://hackerthon.altair823.xyz
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:12:18 +09:00
th-kim0823
02e186a860 fix: UX — start.sh 자동 LAN IP 감지 + topics/vote 레이아웃 조정
- start.sh: 호스트 LAN IP 자동 감지 후 PUBLIC_BASE_URL 세팅, 이제 QR이 172.x 컨테이너 IP 대신 실제 LAN IP를 가리킴
- docker-compose.yml: PUBLIC_BASE_URL 환경변수 pass-through 추가
- app.py: topics min-height 480→360, font-size/line-height 상향, vote counter를 QR 위로 이동, pct 계산 단순화
- README: 실행 섹션 교체 (start.sh 권장, raw/최소 방식 병기)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 21:07:11 +09:00
th-kim0823
02e67baa77 fix: 큰 화면 CSS 라이트 모드 대응
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:56:02 +09:00
th-kim0823
50615e0069 fix: admin URL labels + topics-empty 안내문 + README 테스트 수
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:31:24 +09:00
th-kim0823
a0774ff0d3 fix: assign_teams seed에 current_stage 포함 (디스크 JSON 완전성)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:22:41 +09:00
th-kim0823
56c273779e docs: README — 새 흐름 + URL + topics 설명
Stage 1-6 흐름 갱신 (팀편성 → 안내 → 해킹 → 발표 → 투표 → 시상)
모바일 투표 QR URL 명시 (/?mode=vote)
topics.categories + settings.current_stage 데이터 구조 설명 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:20:19 +09:00
th-kim0823
918fac2742 docs: show-urls.sh — 큰 화면 + 모바일 vote URL 추가 2026-04-27 20:19:25 +09:00
12 changed files with 179 additions and 360 deletions

View File

@@ -1,2 +0,0 @@
ADMIN_TOKEN=강한-랜덤-토큰-여기-넣기
PORT=8501

6
.gitignore vendored
View File

@@ -5,6 +5,10 @@ venv/
*.db
*.sqlite
.env
!.env.example
.streamlit/secrets.toml
.superpowers/
# 행사 진행 중 생성되는 state (assign_teams.py가 entrypoint에서 자동 시드)
data/
results_*.json
teams.md

View File

@@ -10,15 +10,11 @@ ENV TZ=Asia/Seoul
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py assign_teams.py ./
COPY hackathon.json ./
COPY app.py assign_teams.py entrypoint.sh ./
RUN chmod +x /app/entrypoint.sh
EXPOSE 8501
ENV DATA_PATH=/app/hackathon.json
ENV DATA_PATH=/app/data/hackathon.json
CMD ["streamlit", "run", "app.py", \
"--server.address=0.0.0.0", \
"--server.port=8501", \
"--server.headless=true", \
"--browser.gatherUsageStats=false"]
ENTRYPOINT ["/app/entrypoint.sh"]

View File

@@ -3,48 +3,59 @@
35명 (34명 참가 + 1명 진행요원) / 7팀 / 3분야 (재미·완성도·실용성) 투표 앱.
**DB 없이 단일 JSON 파일** (`hackathon.json`)에 모든 데이터.
## 흐름
## 흐름 (행사 진행)
1. `assign_teams.py` 실행 → `hackathon.json` 생성 (people 배정)
2. `app.py` 실행 → 본인 이름 + 사번 입력 → 다른 6팀에 3분야 투표
3. 어드민에서 마감 → 시상식 reveal
4. 결과 자동 archive (`results_<ts>.json`)
1. **Stage 1 — 팀 편성 + 안내** (큰 화면 `/`)
2. **Stage 2 — 예시 주제** (큰 화면 `/`, 어드민이 "다음 stage →")
3. **해킹** (앱 외부, 2시간)
4. **발표**
5. **Stage 3 — 투표** (큰 화면에 QR, 모바일 → `/?mode=vote`)
6. **시상** (`/?mode=ceremony&token=mlops2026`)
## 실행 — Docker
```bash
# 1. 팀 배정 (호스트에서 1회)
python3 assign_teams.py
# 2. .env (1회)
cp .env.example .env
# ADMIN_TOKEN을 강한 토큰으로 변경
# 빠르게: python3 -c "import secrets; print(secrets.token_urlsafe(16))"
# 3. 컨테이너
docker compose up -d --build
# 종료
docker compose down
./start.sh # LAN IP 자동 감지 + PUBLIC_BASE_URL 세팅 + 컨테이너 기동
```
또는 raw 방식 (LAN IP 수동):
```bash
PUBLIC_BASE_URL="http://192.168.0.47:8501" docker compose up -d --build
```
또는 최소 (LAN IP 없이, 어드민에서 나중에 override):
```bash
docker compose up -d --build
```
첫 부팅 시 `entrypoint.sh``assign_teams.py`를 자동 실행해서 `./data/hackathon.json` 시드 생성. 이후 부팅은 기존 데이터 보존.
```bash
docker compose down # 종료 (데이터 보존)
```
**ADMIN_TOKEN**: `mlops2026` (외우기 쉬운 고정값). 변경하려면 `docker-compose.yml``ADMIN_TOKEN:` 값 직접 수정 후 재기동.
## URL
```bash
./show-urls.sh # localhost + LAN IP 포함 모든 URL 출력
```
- 투표: `http://<서버>:8501/`
- 어드민: `http://<서버>:8501/?mode=admin&token=<TOKEN>`
- 시상식: `http://<서버>:8501/?mode=ceremony&token=<TOKEN>`
- 큰 화면: `http://<서버>:8501/`
- 모바일 투표 (QR target): `http://<서버>:8501/?mode=vote`
- 어드민: `http://<서버>:8501/?mode=admin&token=mlops2026`
- 시상식: `http://<서버>:8501/?mode=ceremony&token=mlops2026`
- JSON 원본: `http://<서버>:8501/?mode=raw&token=mlops2026`
macOS 빠른 열기:
```bash
TOKEN=$(grep ADMIN_TOKEN .env | cut -d= -f2)
open "http://localhost:8501/?mode=admin&token=${TOKEN}"
open "http://localhost:8501/?mode=admin&token=mlops2026"
```
## 데이터 파일 — `hackathon.json`
## 데이터 파일 — `./data/hackathon.json`
```json
{
@@ -65,9 +76,12 @@ open "http://localhost:8501/?mode=admin&token=${TOKEN}"
```
- 호스트에서 직접 편집 가능 (jq, vi 등). 앱이 매 요청 reload — 핫리로드.
- 단일 파일 read-write mount. atomic write (tmp + rename).
- `./data/` 디렉터리 read-write mount. atomic write (tmp + rename).
- 행사 전 명단 변경: `people[*].team` 값만 바꾸면 즉시 반영.
- `assign_teams.py` 재실행 시 `people`만 갱신. votes/titles/tie_breaks 보존.
- 어드민 페이지 또는 `?mode=raw&token=...`에서 JSON 다운로드 가능.
- 시드 재생성: `rm data/hackathon.json && docker compose up -d` (entrypoint 자동 실행).
- `topics.categories` 4 카테고리 × 10 items. 어드민에서 form / JSON 둘 다 편집.
- `settings.current_stage` ∈ {"intro","topics","vote"} — 어드민에서 stage 컨트롤.
## 운영 흐름
@@ -84,7 +98,7 @@ docker cp tests/e2e.py hackathon-vote:/tmp/e2e.py
docker exec hackathon-vote python3 /tmp/e2e.py
```
12개 시나리오 검증 (로드, 마감 토글, winner, priority, 동률, 추첨, UNIQUE, 제목, archive, atomic, clear).
19개 시나리오 검증 (로드, 마감 토글, winner, priority, 동률, 추첨, UNIQUE, 제목, archive, atomic, clear).
## 시상 매핑

107
app.py
View File

@@ -29,44 +29,56 @@ CATEGORIES = [
("polish_team", "🏆 완성도상", "양우산 5개"),
("utility_team", "🛠 실용성상", "팜레스트 5개"),
]
TITLE_PLACEHOLDERS = [
"예: Slack 멘션 자동 분류기",
"예: kubectl 매크로 CLI",
"예: PR 우선순위 큐",
"예: 회의 캘린더 브리핑",
"예: 신입 가이드 봇",
"예: 배포 알림 봇",
"예: 점심 투표 봇",
]
PRIZE_PRIORITY = ["utility_team", "polish_team", "fun_team"]
SHOW_CSS = """
<style>
.show-stage-title { font-size: 64px; text-align: center; padding: 16px 0; font-weight: 800; }
.show-stage-sub { font-size: 24px; text-align: center; color: #888; padding-bottom: 24px; }
.show-stage-sub { font-size: 24px; text-align: center; color: #555; padding-bottom: 24px; }
.show-team-card {
font-size: 24px;
padding: 18px 14px;
border-radius: 14px;
background: #1a1a2a;
border: 1px solid #333;
background: #f4f4f8;
border: 1px solid #ddd;
min-height: 220px;
color: #222;
}
.show-team-name { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 12px; color: #ffb84d; }
.show-team-member { font-size: 22px; line-height: 1.6; text-align: center; }
.show-team-name { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 12px; color: #d97706; }
.show-team-member { font-size: 22px; line-height: 1.6; text-align: center; color: #222; }
.show-info-box {
font-size: 26px;
padding: 22px;
border-radius: 12px;
background: #1f1f2f;
background: #f0f4f8;
border: 1px solid #d8e0e8;
margin-top: 24px;
color: #222;
}
.show-cat-card {
border-radius: 14px;
padding: 18px;
min-height: 480px;
min-height: 360px;
}
.show-cat-T1 { background: linear-gradient(135deg, #ffb84d, #ff8c00); color: #222; }
.show-cat-T2 { background: linear-gradient(135deg, #4dffd2, #2a8e7e); color: #1a1a1a; }
.show-cat-T3 { background: linear-gradient(135deg, #ff4d6d, #b83a55); color: white; }
.show-cat-T4 { background: linear-gradient(135deg, #a64dff, #6a2eaf); color: white; }
.show-cat-title { font-size: 32px; font-weight: 800; margin-bottom: 4px; }
.show-cat-tagline { font-size: 16px; font-style: italic; margin-bottom: 4px; }
.show-cat-title { font-size: 36px; font-weight: 800; margin-bottom: 4px; }
.show-cat-tagline { font-size: 18px; font-style: italic; margin-bottom: 4px; }
.show-cat-tone { font-size: 14px; opacity: 0.85; margin-bottom: 12px; }
.show-cat-item { font-size: 17px; line-height: 1.45; padding: 4px 0; }
.show-cat-item { font-size: 19px; line-height: 1.55; padding: 4px 0; }
.show-vote-counter { font-size: 96px; text-align: center; font-weight: 900; padding: 16px 0; }
.show-vote-caption { font-size: 36px; text-align: center; color: #ccc; padding: 12px 0; }
.show-vote-caption { font-size: 36px; text-align: center; color: #555; padding: 12px 0; }
</style>
"""
@@ -465,6 +477,17 @@ def render_stage_vote(data):
unsafe_allow_html=True,
)
votes = data.get("votes", [])
total = len(data.get("people", []))
voted = len(votes)
pct = voted / total if total else 0
st.markdown(
f'<div class="show-vote-counter">{voted} / {total}</div>',
unsafe_allow_html=True,
)
st.progress(pct)
vote_url = compute_vote_url()
qr_png = make_qr_png(vote_url)
@@ -476,16 +499,6 @@ def render_stage_vote(data):
unsafe_allow_html=True,
)
votes = data.get("votes", [])
total = len(data.get("people", []))
voted = len(votes)
pct = int(100 * voted / total) if total else 0
st.markdown(
f'<div class="show-vote-counter">{voted} / {total}</div>',
unsafe_allow_html=True,
)
st.progress(pct / 100 if total else 0)
def render_voter():
if not can_accept_votes(load_data()):
@@ -597,7 +610,8 @@ def render_admin():
with st.expander("🔗 다른 페이지 URL"):
st.markdown(
f"""
- 👥 **참가자 투표**: [/](/)
- 🖥 **큰 화면 (발표자)**: [/](/)
- 📱 **모바일 투표 (QR target)**: [/?mode=vote](?mode=vote)
- 🎉 **시상식 (큰 화면)**: [/?mode=ceremony&token=...](?mode=ceremony&token={ADMIN_TOKEN})
- 📦 **JSON 원본 조회**: [/?mode=raw&token=...](?mode=raw&token={ADMIN_TOKEN})
@@ -630,26 +644,22 @@ def render_admin():
stage_order = list(VALID_STAGES) # ("intro", "topics", "vote")
idx = stage_order.index(cur) if cur in stage_order else 0
sc1, sc2, sc3 = st.columns(3)
sc1, sc2 = st.columns(2)
with sc1:
if st.button("← 이전 stage", disabled=(idx == 0)):
if st.button(
"← 이전 stage",
disabled=(idx == 0),
use_container_width=True,
):
set_stage(stage_order[idx - 1])
st.rerun()
with sc2:
chosen = st.radio(
"직접 선택",
stage_order,
index=idx,
horizontal=True,
label_visibility="collapsed",
key="stage_radio",
)
if chosen != cur:
if st.button("적용", key="stage_apply"):
set_stage(chosen)
st.rerun()
with sc3:
if st.button("다음 stage →", disabled=(idx == len(stage_order) - 1)):
if st.button(
"다음 stage →",
disabled=(idx == len(stage_order) - 1),
type="primary",
use_container_width=True,
):
set_stage(stage_order[idx + 1])
st.rerun()
@@ -663,7 +673,7 @@ def render_admin():
new_override = st.text_input(
"Override (비워두면 자동 감지)",
value=cur_override,
placeholder="http://192.168.1.10:8501",
placeholder="https://hackerthon.altair823.xyz",
key="qr_override",
)
if st.button("Override 저장"):
@@ -671,9 +681,11 @@ def render_admin():
st.success("저장됨.")
st.rerun()
st.divider()
st.subheader("🗳 투표 상태")
voting_open = is_voting_open()
cur_label = "🟢 투표 진행 중" if voting_open else "🔴 투표 마감됨"
st.markdown(f"### 투표 상태: {cur_label}")
st.markdown(f"**현재**: {cur_label}")
cc1, cc2 = st.columns(2)
with cc1:
if voting_open and st.button("🛑 투표 마감 (시상 시작 전 필수)", type="primary"):
@@ -688,7 +700,11 @@ def render_admin():
st.subheader("🗒 주제 편집")
cur_topics = get_topics()
if not cur_topics:
st.warning("주제 시드 비어있음. 컨테이너 재시작 시 시드 자동 적용됨.")
st.warning(
"주제 비어있음. 어드민에서 직접 입력하거나, "
"데이터 파일을 통째로 재시드하려면: "
"`rm data/hackathon.json && docker compose restart vote`"
)
else:
edit_mode = st.radio(
"편집 모드", ["Form", "JSON 직접 편집"], horizontal=True, key="topics_mode"
@@ -798,11 +814,11 @@ def render_admin():
titles = get_titles()
with st.form("titles_form"):
new_titles = {}
for team in TEAMS:
for i, team in enumerate(TEAMS):
new_titles[team] = st.text_input(
team,
value=titles.get(team, ""),
placeholder="예: 슬랙 멘션 자동 분류기",
placeholder=TITLE_PLACEHOLDERS[i % len(TITLE_PLACEHOLDERS)],
key=f"title_{team}",
)
if st.form_submit_button("제목 저장"):
@@ -905,7 +921,10 @@ def render_admin():
st.divider()
st.subheader("🎤 시상식 발표용 (복사해서 화면 공유)")
st.code("\n".join(public_lines), language="markdown")
if public_lines:
st.code("\n".join(public_lines), language="markdown")
else:
st.caption("아직 결과 없음. 투표 마감 후 자동 표시.")
st.divider()
with st.expander("🔍 감사 로그 (사칭 추적용)"):

View File

@@ -349,7 +349,7 @@ def main():
data = json.loads(data_path.read_text(encoding="utf-8"))
else:
data = {
"settings": {"voting_open": True},
"settings": {"voting_open": True, "current_stage": "intro"},
"titles": {},
"tie_breaks": {},
"votes": [],
@@ -358,12 +358,15 @@ def main():
ensure_topics_seeded(data) # 신규
# 누락 키 보강
for k, v in [
("settings", {"voting_open": True}),
("settings", {"voting_open": True, "current_stage": "intro"}),
("titles", {}),
("tie_breaks", {}),
("votes", []),
]:
data.setdefault(k, v)
# 기존 settings에 누락된 nested 키 보강
data["settings"].setdefault("voting_open", True)
data["settings"].setdefault("current_stage", "intro")
data_path.write_text(
json.dumps(data, ensure_ascii=False, indent=2),

View File

@@ -6,10 +6,13 @@ services:
ports:
- "${PORT:-8501}:8501"
environment:
ADMIN_TOKEN: ${ADMIN_TOKEN:-change-me}
DATA_PATH: /app/hackathon.json
# 외우기 쉬운 고정 token. 변경하려면 여기 값만 수정.
ADMIN_TOKEN: mlops2026
DATA_PATH: /app/data/hackathon.json
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-https://hackerthon.altair823.xyz}
volumes:
# 단일 데이터 파일. 호스트 ↔ 컨테이너 read-write mount.
# 호스트에서 jq/vi 편집 가능, 앱이 votes 추가 시 그대로 반영.
- ./hackathon.json:/app/hackathon.json
# 단일 데이터 디렉터리 마운트.
# 첫 부팅 시 entrypoint.sh가 assign_teams.py 실행하여 hackathon.json 시드.
# 이후 부팅은 기존 파일 보존.
- ./data:/app/data
restart: unless-stopped

21
entrypoint.sh Normal file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
set -e
# DATA_PATH 비어있으면 assign_teams.py로 부트스트랩.
# 호스트 마운트(./data)에 처음 부팅 시 1회 실행.
if [ ! -s "$DATA_PATH" ]; then
echo "[init] $DATA_PATH 없음/비어있음 → assign_teams.py 실행"
mkdir -p "$(dirname "$DATA_PATH")"
cd /app
python3 assign_teams.py
mv /app/hackathon.json "$DATA_PATH"
echo "[init] 시드 완료 → $DATA_PATH"
else
echo "[init] 기존 $DATA_PATH 보존"
fi
exec streamlit run /app/app.py \
--server.address=0.0.0.0 \
--server.port=8501 \
--server.headless=true \
--browser.gatherUsageStats=false

View File

@@ -1,248 +0,0 @@
{
"settings": {
"voting_open": true
},
"titles": {},
"tie_breaks": {},
"votes": [],
"people": [
{
"name": "김호승",
"team": "팀1",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "유준희",
"team": "팀1",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "이준석",
"team": "팀1",
"dept": "MLOps HPC",
"senior": false,
"notes": ""
},
{
"name": "장다현",
"team": "팀1",
"dept": "MLOps System",
"senior": false,
"notes": ""
},
{
"name": "강승형",
"team": "팀1",
"dept": "MLOps Data",
"senior": true,
"notes": ""
},
{
"name": "서한배",
"team": "팀2",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "김민섭",
"team": "팀2",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "유용혁",
"team": "팀2",
"dept": "MLOps HPC",
"senior": false,
"notes": ""
},
{
"name": "박영훈",
"team": "팀2",
"dept": "MLOps System",
"senior": true,
"notes": ""
},
{
"name": "박재호",
"team": "팀2",
"dept": "MLOps Data",
"senior": false,
"notes": ""
},
{
"name": "이성재",
"team": "팀3",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "이재광",
"team": "팀3",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "김영관",
"team": "팀3",
"dept": "MLOps HPC",
"senior": true,
"notes": ""
},
{
"name": "정채윤",
"team": "팀3",
"dept": "MLOps System",
"senior": false,
"notes": ""
},
{
"name": "변수민",
"team": "팀3",
"dept": "MLOps Data",
"senior": true,
"notes": ""
},
{
"name": "심성환",
"team": "팀4",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "유지원",
"team": "팀4",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "오근현",
"team": "팀4",
"dept": "MLOps HPC",
"senior": false,
"notes": ""
},
{
"name": "장혁진",
"team": "팀4",
"dept": "MLOps System",
"senior": false,
"notes": ""
},
{
"name": "손현준",
"team": "팀4",
"dept": "MLOps Data",
"senior": true,
"notes": ""
},
{
"name": "정현준",
"team": "팀5",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "조민정",
"team": "팀5",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "김재현",
"team": "팀5",
"dept": "MLOps HPC",
"senior": false,
"notes": "최주니어"
},
{
"name": "김병훈",
"team": "팀5",
"dept": "MLOps System",
"senior": true,
"notes": ""
},
{
"name": "한지승",
"team": "팀5",
"dept": "MLOps Platform",
"senior": true,
"notes": "지각 가능"
},
{
"name": "이정태",
"team": "팀6",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "최호진",
"team": "팀6",
"dept": "MLOps HPC",
"senior": false,
"notes": ""
},
{
"name": "김정명",
"team": "팀6",
"dept": "MLOps HPC",
"senior": true,
"notes": ""
},
{
"name": "길주현",
"team": "팀6",
"dept": "MLOps System",
"senior": false,
"notes": ""
},
{
"name": "서희",
"team": "팀6",
"dept": "MLOps System",
"senior": true,
"notes": ""
},
{
"name": "이준형",
"team": "팀7",
"dept": "AI Efficiency Tech",
"senior": false,
"notes": ""
},
{
"name": "전효준",
"team": "팀7",
"dept": "MLOps HPC",
"senior": true,
"notes": ""
},
{
"name": "이지환",
"team": "팀7",
"dept": "MLOps System",
"senior": false,
"notes": ""
},
{
"name": "김동국",
"team": "팀7",
"dept": "MLOps Data",
"senior": false,
"notes": ""
}
]
}

View File

@@ -3,26 +3,30 @@
set -euo pipefail
cd "$(dirname "$0")"
if [[ ! -f .env ]]; then
echo "❌ .env 파일 없음. 먼저 'cp .env.example .env' 후 ADMIN_TOKEN 설정."
# docker-compose.yml에서 ADMIN_TOKEN 추출 (환경변수 override 가능)
TOKEN="${ADMIN_TOKEN:-$(grep -E "^[[:space:]]*ADMIN_TOKEN:" docker-compose.yml | head -1 | sed -E 's/^[[:space:]]*ADMIN_TOKEN:[[:space:]]*//' | tr -d '"' | xargs)}"
PORT="${PORT:-8501}"
if [[ -z "$TOKEN" ]]; then
echo "❌ ADMIN_TOKEN 못 찾음. docker-compose.yml 확인."
exit 1
fi
TOKEN=$(grep -E "^ADMIN_TOKEN=" .env | head -1 | cut -d= -f2- | tr -d '"')
PORT=$(grep -E "^PORT=" .env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' || echo "")
PORT=${PORT:-8501}
# LAN IP 자동 감지 (macOS/Linux)
LAN_IP=$(ipconfig getifaddr en0 2>/dev/null || ip -4 addr show 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '^127\.' | head -1 || echo "")
echo "============================================"
echo " 해커톤 투표 URL"
echo " 해커톤 투표 URL (token: ${TOKEN})"
echo "============================================"
echo
echo "👥 참가자 투표:"
echo "🖥 큰 화면 (발표자):"
echo " http://localhost:${PORT}/"
[[ -n "$LAN_IP" ]] && echo " http://${LAN_IP}:${PORT}/ (LAN)"
echo
echo "📱 모바일 투표 (QR target):"
echo " http://localhost:${PORT}/?mode=vote"
[[ -n "$LAN_IP" ]] && echo " http://${LAN_IP}:${PORT}/?mode=vote"
echo
echo "🔐 진행자 어드민:"
echo " http://localhost:${PORT}/?mode=admin&token=${TOKEN}"
[[ -n "$LAN_IP" ]] && echo " http://${LAN_IP}:${PORT}/?mode=admin&token=${TOKEN}"
@@ -31,6 +35,10 @@ echo "🎉 시상식 (큰 화면):"
echo " http://localhost:${PORT}/?mode=ceremony&token=${TOKEN}"
[[ -n "$LAN_IP" ]] && echo " http://${LAN_IP}:${PORT}/?mode=ceremony&token=${TOKEN}"
echo
echo "📦 JSON 원본 조회/다운로드:"
echo " http://localhost:${PORT}/?mode=raw&token=${TOKEN}"
[[ -n "$LAN_IP" ]] && echo " http://${LAN_IP}:${PORT}/?mode=raw&token=${TOKEN}"
echo
echo "============================================"
echo " 팁: 'open' 으로 바로 브라우저 열기 (macOS)"
echo " open \"http://localhost:${PORT}/?mode=admin&token=${TOKEN}\""

16
start.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
# 호스트 LAN IP 자동 감지 → PUBLIC_BASE_URL 세팅 → 컨테이너 기동
set -euo pipefail
cd "$(dirname "$0")"
LAN_IP=$(ipconfig getifaddr en0 2>/dev/null || ip -4 addr show 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '^127\.' | head -1 || echo "")
PORT=${PORT:-8501}
if [[ -n "$LAN_IP" ]]; then
export PUBLIC_BASE_URL="http://${LAN_IP}:${PORT}"
echo "[start] PUBLIC_BASE_URL=${PUBLIC_BASE_URL} (자동 감지)"
else
echo "[start] LAN IP 감지 실패. 어드민에서 PUBLIC_BASE_URL 직접 설정 필요."
fi
exec docker compose up -d --build "$@"

View File

@@ -1,15 +0,0 @@
# 해커톤 팀 배정 (확정)
- 시드: `20260435`
- 총 34명, 7팀, 사이즈 [5, 5, 5, 5, 5, 5, 4]
- 김태현: 진행요원 (참여 X)
- ⭐ 시니어, 🌱 최주니어, ⏰ 지각 가능
| 팀 | 인원 | 시니어 | 멤버 |
|---|---|---|---|
| 팀1 | 5 | 1 | 김호승, 유준희, 이준석, 장다현, 강승형⭐ |
| 팀2 | 5 | 1 | 서한배, 김민섭, 유용혁, 박영훈⭐, 박재호 |
| 팀3 | 5 | 2 | 이성재, 이재광, 김영관⭐, 정채윤, 변수민⭐ |
| 팀4 | 5 | 1 | 심성환, 유지원, 오근현, 장혁진, 손현준⭐ |
| 팀5 | 5 | 2 | 정현준, 조민정, 김재현🌱, 김병훈⭐, 한지승⭐⏰ |
| 팀6 | 5 | 2 | 이정태, 최호진, 김정명⭐, 길주현, 서희⭐ |
| 팀7 | 4 | 1 | 이준형, 전효준⭐, 이지환, 김동국 |