Compare commits
10 Commits
3373f5729f
...
addde1a0ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
addde1a0ea | ||
|
|
447f067ae9 | ||
|
|
75aa0349ff | ||
|
|
e872b841e3 | ||
|
|
02e186a860 | ||
|
|
02e67baa77 | ||
|
|
50615e0069 | ||
|
|
a0774ff0d3 | ||
|
|
56c273779e | ||
|
|
918fac2742 |
@@ -1,2 +0,0 @@
|
||||
ADMIN_TOKEN=강한-랜덤-토큰-여기-넣기
|
||||
PORT=8501
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,6 +5,10 @@ venv/
|
||||
*.db
|
||||
*.sqlite
|
||||
.env
|
||||
!.env.example
|
||||
.streamlit/secrets.toml
|
||||
.superpowers/
|
||||
|
||||
# 행사 진행 중 생성되는 state (assign_teams.py가 entrypoint에서 자동 시드)
|
||||
data/
|
||||
results_*.json
|
||||
teams.md
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -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"]
|
||||
|
||||
68
README.md
68
README.md
@@ -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
107
app.py
@@ -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("🔍 감사 로그 (사칭 추적용)"):
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
21
entrypoint.sh
Normal 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
|
||||
248
hackathon.json
248
hackathon.json
@@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
24
show-urls.sh
24
show-urls.sh
@@ -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
16
start.sh
Executable 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 "$@"
|
||||
15
teams.md
15
teams.md
@@ -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 | 이준형, 전효준⭐, 이지환, 김동국 |
|
||||
Reference in New Issue
Block a user