# -*- 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 (끄기만) --port N : 띄울 포트(기본 = 개발 포트 − 1000) --replace-dev : 개발판 자리(개발 포트)에 띄움 — 빌드가 된 뒤에만 그 포트의 vite dev 를 끔 · 빌드 실패면 아무것도 안 끄고 1 로 끝남(개발판 그대로) · `.claude/dev_up.py --build` 가 부름 - 개발 서버(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 free_port(port: int) -> None: """그 포트를 물고 있는 프로세스(개발판 vite)를 끈다 — 짝 = `_free_dev_port`(config).""" out = subprocess.run( ["netstat", "-ano", "-p", "TCP"], capture_output=True, text=True, errors="replace" ).stdout for line in out.splitlines(): cols = line.split() if ( len(cols) >= 5 and cols[1].endswith(f":{port}") and cols[3] == "LISTENING" and cols[4].isdigit() ): subprocess.run( ["taskkill", "/T", "/F", "/PID", cols[4]], capture_output=True, check=False ) def arg(name: str) -> str | None: return sys.argv[sys.argv.index(name) + 1] if name in sys.argv[:-1] else None def spawn(cmd: list[str], env: dict[str, str]) -> subprocess.Popen: log = open(LOG, "ab") # noqa: SIM115 — 자식 프로세스가 물려받는다 flags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP return subprocess.Popen( cmd, cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT, creationflags=flags ) 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) replace = "--replace-dev" in sys.argv if not replace: 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 = int(arg("--port") or (vite if replace else vite - 1000)) env = {**os.environ, "AISLO_API_PORT": str(api), "FRONTEND_DEV_PORT": str(vite)} try: run(["node", VITE, "build", "--configLoader", "runner"], env) run(["npm", "run", "build:b07-cad"], env) except subprocess.CalledProcessError as exc: # 옛 빌드판이 자리에 있으면 파일 목록이 낡음 — 끄고 개발판으로 if replace and os.path.exists(PID_FILE): stop() free_port(port) time.sleep(1) spawn(["node", VITE, "--configLoader", "runner", "--host", "--port", str(port)], env) print(f"PREVIEW=BUILD_FAIL ({exc.returncode}) — 옛 빌드판을 끄고 {port} 개발판") else: print(f"PREVIEW=BUILD_FAIL ({exc.returncode}) — 아무것도 안 끔") return 1 if replace: # 빌드가 된 뒤에만 — 옛 빌드판 · 그 포트의 개발판을 끔 stop() free_port(port) time.sleep(1) process = spawn( ["node", VITE, "preview", "--configLoader", "runner", "--port", str(port), "--strictPort"], env, ) 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())