feat: QR PNG 생성 + vote URL resolver

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-04-27 19:54:31 +09:00
parent 546bd54700
commit 874de0a46d
2 changed files with 66 additions and 0 deletions

32
app.py
View File

@@ -11,8 +11,11 @@ import os
import random as _rand import random as _rand
import threading import threading
from datetime import datetime from datetime import datetime
import socket
from io import BytesIO
from pathlib import Path from pathlib import Path
import qrcode
import streamlit as st import streamlit as st
DATA_PATH = os.environ.get( DATA_PATH = os.environ.get(
@@ -200,6 +203,35 @@ def fmt_team(team, titles):
return f"{team}{t}" if t else team return f"{team}{t}" if t else team
def make_qr_png(url: str, box_size: int = 20) -> bytes:
img = qrcode.make(url, box_size=box_size, border=2)
buf = BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
def _detect_lan_ip() -> str:
"""LAN IP 자동 감지. 실패 시 'localhost'."""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return "localhost"
def compute_vote_url() -> str:
data = load_data()
base = (
data.get("settings", {}).get("public_base_url")
or os.environ.get("PUBLIC_BASE_URL")
or f"http://{_detect_lan_ip()}:8501"
)
return f"{base.rstrip('/')}/?mode=vote"
def compute_winners(): def compute_winners():
"""우선순위 기반 1팀 1상 + 동률 처리.""" """우선순위 기반 1팀 1상 + 동률 처리."""
data = load_data() data = load_data()

View File

@@ -299,6 +299,38 @@ def t_topics_seeded_after_assign():
assert c["tone"] assert c["tone"]
def t_make_qr_png():
from app import make_qr_png
png = make_qr_png("http://localhost:8501/?mode=vote")
# PNG signature 8 bytes
assert png[:8] == b"\x89PNG\r\n\x1a\n"
assert len(png) > 100
def t_compute_vote_url_priority():
import os as _os
from app import compute_vote_url, save_data, load_data, _empty_state
save_data(_empty_state())
# 1. settings.public_base_url 우선
d = load_data()
d["settings"]["public_base_url"] = "http://example.com:9000"
save_data(d)
assert compute_vote_url() == "http://example.com:9000/?mode=vote"
# 2. env fallback
d["settings"].pop("public_base_url")
save_data(d)
_os.environ["PUBLIC_BASE_URL"] = "http://env-host:7777"
assert compute_vote_url() == "http://env-host:7777/?mode=vote"
_os.environ.pop("PUBLIC_BASE_URL")
# 3. localhost / LAN fallback
url = compute_vote_url()
assert url.endswith("/?mode=vote")
assert url.startswith("http://")
if __name__ == "__main__": if __name__ == "__main__":
print(f"# E2E (data={TEST_DATA})\n") print(f"# E2E (data={TEST_DATA})\n")
test("hackathon.json 로드 (34명, 7팀)", t_load) test("hackathon.json 로드 (34명, 7팀)", t_load)
@@ -318,6 +350,8 @@ if __name__ == "__main__":
test("stage 헬퍼", t_stage_helpers) test("stage 헬퍼", t_stage_helpers)
test("topics 헬퍼", t_topics_helpers) test("topics 헬퍼", t_topics_helpers)
test("topics 시드", t_topics_seeded_after_assign) test("topics 시드", t_topics_seeded_after_assign)
test("QR PNG 생성", t_make_qr_png)
test("vote URL 우선순위", t_compute_vote_url_priority)
fails = sum(1 for r, _ in results if r == FAIL) fails = sum(1 for r, _ in results if r == FAIL)
print(f"\n# {len(results)} 중 통과 {len(results) - fails}, 실패 {fails}") print(f"\n# {len(results)} 중 통과 {len(results) - fails}, 실패 {fails}")