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>
This commit is contained in:
2026-10-28 12:08:47 +09:00
co-authored by Claude Opus 5.5
parent fb6ee44f18
commit 61b799eefd
2 changed files with 131 additions and 13 deletions
+59 -13
View File
@@ -3,6 +3,10 @@
실행: ./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 · 폰)에서 첫 화면이 늦다 —
빌드판은 묶은 파일 몇 개만 보낸다.
@@ -54,6 +58,36 @@ def stop() -> None:
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")
@@ -61,7 +95,9 @@ def run(cmd: list[str], env: dict[str, str]) -> None:
def main() -> int:
os.makedirs(TMP, exist_ok=True)
stop()
replace = "--replace-dev" in sys.argv
if not replace:
stop()
if "--stop" in sys.argv:
print("PREVIEW=stopped")
return 0
@@ -69,21 +105,31 @@ def main() -> int:
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
port = int(arg("--port") or (vite if replace else 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)
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)
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,
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))