From 02e186a8609e1f7089624b85733abf43808a4b28 Mon Sep 17 00:00:00 2001
From: th-kim0823
Date: Mon, 27 Apr 2026 21:07:11 +0900
Subject: [PATCH] =?UTF-8?q?fix:=20UX=20=E2=80=94=20start.sh=20=EC=9E=90?=
=?UTF-8?q?=EB=8F=99=20LAN=20IP=20=EA=B0=90=EC=A7=80=20+=20topics/vote=20?=
=?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=A1=B0=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
README.md | 16 ++++++++++++++--
app.py | 29 +++++++++++++++--------------
docker-compose.yml | 13 ++++++++-----
start.sh | 16 ++++++++++++++++
4 files changed, 53 insertions(+), 21 deletions(-)
create mode 100755 start.sh
diff --git a/README.md b/README.md
index 6ac3997..db04665 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,19 @@
5. **Stage 3 — 투표** (큰 화면에 QR, 모바일 → `/?mode=vote`)
6. **시상** (`/?mode=ceremony&token=mlops2026`)
-## 실행 — Docker (한 줄)
+## 실행 — Docker
+
+```bash
+./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
@@ -24,7 +36,7 @@ docker compose up -d --build
docker compose down # 종료 (데이터 보존)
```
-**ADMIN_TOKEN**: `mlops2026` (외우기 쉬운 고정값). 변경하려면 `docker-compose.yml`의 `ADMIN_TOKEN:` 값 직접 수정 후 `docker compose up -d --build`.
+**ADMIN_TOKEN**: `mlops2026` (외우기 쉬운 고정값). 변경하려면 `docker-compose.yml`의 `ADMIN_TOKEN:` 값 직접 수정 후 재기동.
## URL
diff --git a/app.py b/app.py
index ee4f6df..7ee5416 100644
--- a/app.py
+++ b/app.py
@@ -58,16 +58,16 @@ SHOW_CSS = """
.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: #555; padding: 12px 0; }
@@ -468,6 +468,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'{voted} / {total}
',
+ unsafe_allow_html=True,
+ )
+ st.progress(pct)
+
vote_url = compute_vote_url()
qr_png = make_qr_png(vote_url)
@@ -479,16 +490,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'{voted} / {total}
',
- unsafe_allow_html=True,
- )
- st.progress(pct / 100 if total else 0)
-
def render_voter():
if not can_accept_votes(load_data()):
diff --git a/docker-compose.yml b/docker-compose.yml
index 8147f02..ac5c830 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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:-}
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
diff --git a/start.sh b/start.sh
new file mode 100755
index 0000000..1f2abf2
--- /dev/null
+++ b/start.sh
@@ -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 "$@"