Files
Aislo/tools/preview_up.py
T
eomsangdonandClaude Opus 5.5 61b799eefd feat(tools): 빌드판 포트 지정 · 개발판 자리에 빌드판 (PLAN 570-1 · 요구 398)
- preview_up.py --port N · --replace-dev(빌드가 된 뒤에만 개발 포트 vite dev 를 끄고 그 자리에 빌드판)
- 빌드 실패 = 아무것도 안 끔 · 옛 빌드판이 그 자리면 끄고 개발판으로
- 기본(개발 포트 − 1000) 그대로 · 지킴 시험 test_570_1_preview_up
- (git 밖 .claude) dev_up.py --build · git-sync-all.ps1 -Global 데스크탑 메인 = WMI 로 dev_up --build · 5173 빌드판 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:08:47 +09:00

151 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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())