feat: QR PNG 생성 + vote URL resolver
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
32
app.py
32
app.py
@@ -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()
|
||||||
|
|||||||
34
tests/e2e.py
34
tests/e2e.py
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user