From a09ac1c42d161db310f8b9d51c482970242ae06e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 27 Oct 2026 06:56:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(=EB=AA=A8=EB=B0=94=EC=9D=BC):=20=EB=B6=88?= =?UTF-8?q?=EB=9F=AC=EC=98=A4=EB=8A=94=20=EC=A4=91=20=ED=8C=90=20=C2=B7=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=ED=8C=90=20=EC=84=9C=EB=B2=84=20=C2=B7=20CAD?= =?UTF-8?q?=20=EA=B8=80=EA=BC=B4=20404=20(PLAN=20555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- A00_Common/index.html | 56 ++++++++++ B07_DesignDetail/openwebcad/vite.config.ts | 22 +++- .../tester/test_555_boot_loader_preview.py | 38 +++++++ tools/preview_up.py | 104 ++++++++++++++++++ vite.config.ts | 10 +- 5 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 resources/tester/test_555_boot_loader_preview.py create mode 100644 tools/preview_up.py diff --git a/A00_Common/index.html b/A00_Common/index.html index 1a5d34f33..e992b33a3 100644 --- a/A00_Common/index.html +++ b/A00_Common/index.html @@ -4,9 +4,65 @@ 임도 설계 및 견적 자동화 + + + + +
+
+
불러오는 중…
+
연결이 느림 — 조금 더 기다리거나 새로고침
+
diff --git a/B07_DesignDetail/openwebcad/vite.config.ts b/B07_DesignDetail/openwebcad/vite.config.ts index 231cd06f3..7dc60640b 100644 --- a/B07_DesignDetail/openwebcad/vite.config.ts +++ b/B07_DesignDetail/openwebcad/vite.config.ts @@ -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: { diff --git a/resources/tester/test_555_boot_loader_preview.py b/resources/tester/test_555_boot_loader_preview.py new file mode 100644 index 000000000..87ae01de1 --- /dev/null +++ b/resources/tester/test_555_boot_loader_preview.py @@ -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 diff --git a/tools/preview_up.py b/tools/preview_up.py new file mode 100644 index 000000000..d745fef31 --- /dev/null +++ b/tools/preview_up.py @@ -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()) diff --git a/vite.config.ts b/vite.config.ts index c9fbe8032..b5aba0b4f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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,