feat(모바일): 불러오는 중 판 · 빌드판 서버 · CAD 글꼴 404 (PLAN 555)

- index.html — 첫 페이지 전까지 「불러오는 중」(테마 바탕 · 저장 테마 · 시스템 어두움) · 20 s 뒤 느린 연결 안내 · favicon 요청 없앰
- tools/preview_up.py — 앱 · CAD 빌드 뒤 vite preview(포트 = 개발 포트 − 1000 · /api · /b07-cad 프록시)
- CAD 빌드에 공용 글꼴 복사 — Tailwind 가 url 을 고치지 않아 dist/assets 에서 찾던 404
- 시험 test_555_boot_loader_preview.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 06:56:26 +09:00
co-authored by Claude Opus 5.5
parent e37045010a
commit a09ac1c42d
5 changed files with 227 additions and 3 deletions
+56
View File
@@ -4,9 +4,65 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>임도 설계 및 견적 자동화</title>
<link rel="icon" href="data:," />
<!-- 고른 테마를 첫 그림 전에(앱 initThemeAndLang 과 같은 열쇠) · 고른 게 없으면 불러오는 중 판만
시스템 설정(어두움)을 따름 -->
<script>
try {
var t = localStorage.getItem("frd_theme");
if (t === "dark" || t === "light") document.documentElement.setAttribute("data-theme", t);
if (t === "dark" || (!t && matchMedia("(prefers-color-scheme: dark)").matches))
document.documentElement.setAttribute("data-boot-dark", "");
} catch (e) {}
</script>
<!-- 불러오는 중(555-1) — 테마 css 가 오기 전이라 색을 박음(--color-bg · --color-text-muted 값) ·
첫 페이지가 #app-outlet 에 그려지면 숨김 · 20 s 뒤 느린 연결 안내 · 도는 그림 없음 -->
<style>
#boot-loader {
position: fixed;
inset: 0;
z-index: 2147483000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
background: #f6f7f9;
color: #6b7280;
font:
15px/1.5 "Noto Sans KR",
"Malgun Gothic",
"Apple SD Gothic Neo",
sans-serif;
text-align: center;
padding: 16px;
}
[data-boot-dark] #boot-loader {
color-scheme: dark;
background: #121417;
color: #9aa1ab;
}
#boot-loader-slow {
visibility: hidden;
font-size: 13px;
animation: boot-loader-slow 0s 20s forwards;
}
@keyframes boot-loader-slow {
to {
visibility: visible;
}
}
body:has(#app-outlet > *) #boot-loader {
display: none;
}
</style>
</head>
<body>
<div id="app"></div>
<div id="boot-loader" role="status">
<div>불러오는 중…</div>
<div id="boot-loader-slow">연결이 느림 — 조금 더 기다리거나 새로고침</div>
</div>
<script type="module" src="./main.ts"></script>
</body>
</html>
+20 -2
View File
@@ -1,11 +1,29 @@
import { cpSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
import { defineConfig, type Plugin } from 'vite';
import svgr from 'vite-plugin-svgr'; // https://vitejs.dev/config/
// 공용 글꼴(ui_template/fonts) — Tailwind 가 테마 css 를 끌어 붙이며 url(./noto-sans-kr-*.woff2)
// 를 고쳐 쓰지 않아 dist/assets 에서 찾음(555-3) · 그 자리에 파일을 둠.
const aisloFonts: Plugin = {
name: 'aislo-fonts',
apply: 'build',
writeBundle() {
cpSync(
fileURLToPath(new URL('../../ui_template/fonts', import.meta.url)),
fileURLToPath(new URL('./dist/assets', import.meta.url)),
{
recursive: true,
filter: (src) => !src.endsWith('.css'),
}
);
},
};
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), svgr()],
plugins: [react(), svgr(), aisloFonts],
base: './',
resolve: {
alias: {
@@ -0,0 +1,38 @@
"""555 — 불러오는 중 판 · 빌드판 서버 · CAD 글꼴의 짝 대조(파일만 읽음)."""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def read(rel: str) -> str:
return (ROOT / rel).read_text(encoding="utf-8")
def test_boot_loader_hides_on_app_outlet():
html = read("A00_Common/index.html")
assert 'id="boot-loader"' in html
assert "body:has(#app-outlet > *) #boot-loader" in html
assert 'outlet.id = "app-outlet"' in read("A00_Common/app_shell.ts")
def test_boot_loader_theme_key_matches_app():
key = re.search(r'THEME_STORAGE_KEY = "([^"]+)"', read("config/config_frontend.ts")).group(1)
assert f'localStorage.getItem("{key}")' in read("A00_Common/index.html")
def test_preview_port_is_dev_minus_1000_and_proxied():
cfg = read("vite.config.ts")
preview = re.search(r"preview: \{(.*?)\n \}", cfg, re.S).group(1)
assert "port: devPort - 1000" in preview and "strictPort: true" in preview
assert "proxy" not in preview # server.proxy(/api · /b07-cad)를 물려받음
assert '"/api"' in cfg and '"/b07-cad"' in cfg
def test_cad_build_copies_shared_fonts():
cfg = read("B07_DesignDetail/openwebcad/vite.config.ts")
assert "../../ui_template/fonts" in cfg and "./dist/assets" in cfg
fonts_css = read("ui_template/fonts/noto_sans_kr.css")
for name in re.findall(r'url\("\./([^"]+)"\)', fonts_css):
assert (ROOT / "ui_template/fonts" / name).is_file(), name
+104
View File
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
"""빌드판 서버(555-2) — 앱 · CAD 를 빌드하고 `vite preview` 를 띄운다(폰 · 외부 접속용).
실행: ./venv/Scripts/python.exe tools/preview_up.py (빌드 + 띄움 · 수십 초)
./venv/Scripts/python.exe tools/preview_up.py --stop (끄기만)
- 개발 서버(vite)는 파일 수백 개를 하나씩 보내 느린 연결(Tailscale · 폰)에서 첫 화면이 늦다 —
빌드판은 묶은 파일 몇 개만 보낸다.
- 포트 = 개발 포트 − 1000(메인 5173 → 4173 · 서브 5181 → 4181) · 개발 포트 · API 포트는
환경변수(AISLO_VITE_PORT · AISLO_API_PORT) > `OWNERS.md` 이 폴더 줄 > 8000 · 5173.
- /api · /b07-cad 는 같은 폴더 백엔드로 넘김(`vite.config.ts` preview) — 백엔드는 따로 켜 둠.
- 빌드 산출물 = config/node_modules/.build · CAD dist(둘 다 git 밖) · 로그 tmp/preview.log.
- 다시 부르면 옛 것을 끄고 새로 빌드해 띄움(preview 는 띄울 때의 파일 목록을 기억).
"""
import os
import re
import subprocess
import sys
import time
import urllib.request
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TMP = os.path.join(ROOT, "tmp")
PID_FILE = os.path.join(TMP, "preview.pid")
LOG = os.path.join(TMP, "preview.log")
VITE = os.path.join(ROOT, "config", "node_modules", "vite", "bin", "vite.js")
def owners_ports() -> tuple[int, int] | None:
"""OWNERS.md 표에서 이 폴더 줄의 (API, vite) 포트 — 짝 = `.claude/dev_up.py`."""
try:
with open(os.path.join(ROOT, "OWNERS.md"), encoding="utf-8-sig") as owners:
lines = owners.read().splitlines()
except OSError:
return None
here = os.path.normcase(os.path.normpath(ROOT))
for line in lines:
cells = [cell.strip().strip("`") for cell in line.strip().strip("|").split("|")]
if len(cells) >= 5 and os.path.normcase(os.path.normpath(cells[2])) == here:
ports = re.findall(r"\d{4,5}", cells[4])
if len(ports) == 2:
return int(ports[0]), int(ports[1])
return None
def stop() -> None:
try:
with open(PID_FILE, encoding="utf-8") as f:
pid = int(f.read().strip())
except (OSError, ValueError):
return
subprocess.run(["taskkill", "/T", "/F", "/PID", str(pid)], capture_output=True, check=False)
os.remove(PID_FILE)
def run(cmd: list[str], env: dict[str, str]) -> None:
print("RUN", " ".join(cmd), flush=True)
subprocess.run(cmd, cwd=ROOT, env=env, check=True, shell=os.name == "nt" and cmd[0] == "npm")
def main() -> int:
os.makedirs(TMP, exist_ok=True)
stop()
if "--stop" in sys.argv:
print("PREVIEW=stopped")
return 0
api, vite = owners_ports() or (8000, 5173)
api = int(os.environ.get("AISLO_API_PORT") or api)
vite = int(os.environ.get("AISLO_VITE_PORT") or vite)
port = vite - 1000
env = {**os.environ, "AISLO_API_PORT": str(api), "FRONTEND_DEV_PORT": str(vite)}
run(["node", VITE, "build", "--configLoader", "runner"], env)
run(["npm", "run", "build:b07-cad"], env)
log = open(LOG, "ab") # noqa: SIM115 — 자식 프로세스가 물려받는다
flags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
process = subprocess.Popen(
["node", VITE, "preview", "--configLoader", "runner"],
cwd=ROOT,
env=env,
stdout=log,
stderr=subprocess.STDOUT,
creationflags=flags,
)
with open(PID_FILE, "w", encoding="utf-8") as f:
f.write(str(process.pid))
url = f"http://127.0.0.1:{port}/"
for _ in range(60):
try:
urllib.request.urlopen(url, timeout=2)
print(f"PREVIEW=up {url} (API {api}) · 로그 {LOG}")
return 0
except OSError:
time.sleep(0.5)
print(f"PREVIEW=TIMEOUT — {LOG} 확인")
return 1
if __name__ == "__main__":
sys.exit(main())
+9 -1
View File
@@ -8,6 +8,7 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
// 저장이 실패하고, 부팅 때 죽으면 화면이 통째로 백지가 된다(2026-09-07 실측:
// `127.0.0.1:8001` 200 · `[::1]:8001` 연결 실패). 숫자로 박아 그 갈림을 없앤다.
const apiTarget = `http://127.0.0.1:${process.env.AISLO_API_PORT ?? "8000"}`;
const devPort = Number(process.env.FRONTEND_DEV_PORT ?? 5173);
/**
* Vite 설정 — 임도 설계 웹앱 (Vanilla TS + Three.js)
@@ -37,7 +38,7 @@ export default {
server: {
// main.py가 CLI --port로 덮지만, `npm run dev` 맨손 실행에서 보조 워크트리가
// 메인의 5173을 뺏지 않도록 env를 먼저 본다(dev_up.py가 주입한다).
port: Number(process.env.FRONTEND_DEV_PORT ?? 5173),
port: devPort,
open: false,
// 백엔드(FastAPI) 프록시 — config_frontend.ts의 API_BASE_URL과 정합.
// 포트는 AISLO_API_PORT로 바꿀 수 있다(기본 8000) — 워크트리를 나눠 쓰는 병행
@@ -54,6 +55,13 @@ export default {
},
},
},
// 빌드판(555-2 · `tools/preview_up.py`) — 폰 · 외부 접속용 · 포트 = 개발 포트 − 1000(5173 → 4173)
// · 프록시(/api · /b07-cad)는 server.proxy 를 그대로 물려받음.
preview: {
port: devPort - 1000,
strictPort: true,
host: true,
},
build: {
outDir: "../config/node_modules/.build",
emptyOutDir: true,