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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user