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
+72
View File
@@ -0,0 +1,72 @@
"""570-1 빌드판 자리 — 기본 = 개발 포트 − 1000 · --replace-dev = 개발 포트 · 빌드 실패면 안 끔."""
import importlib.util
import subprocess
from pathlib import Path
import pytest
SRC = Path(__file__).resolve().parents[2] / "tools" / "preview_up.py"
@pytest.fixture
def pu(monkeypatch, tmp_path):
spec = importlib.util.spec_from_file_location("preview_up_570", SRC)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
calls: list = []
monkeypatch.setattr(mod, "TMP", str(tmp_path))
monkeypatch.setattr(mod, "PID_FILE", str(tmp_path / "preview.pid"))
monkeypatch.setattr(mod, "LOG", str(tmp_path / "preview.log"))
monkeypatch.setattr(mod, "owners_ports", lambda: (8001, 5174))
monkeypatch.setattr(mod, "stop", lambda: calls.append("stop"))
monkeypatch.setattr(mod, "free_port", lambda port: calls.append(("free", port)))
monkeypatch.setattr(mod, "run", lambda cmd, env: None)
monkeypatch.setattr(mod.time, "sleep", lambda s: None)
monkeypatch.setattr(mod.urllib.request, "urlopen", lambda url, timeout: calls.append(url))
class Proc:
pid = 1
monkeypatch.setattr(mod, "spawn", lambda cmd, env: calls.append(("spawn", cmd)) or Proc())
monkeypatch.delenv("AISLO_VITE_PORT", raising=False)
monkeypatch.delenv("AISLO_API_PORT", raising=False)
mod.calls = calls
return mod
def spawned_port(calls) -> str:
cmd = next(c[1] for c in calls if isinstance(c, tuple) and c[0] == "spawn")
return cmd[cmd.index("--port") + 1]
def test_default_dev_minus_1000(pu, monkeypatch):
monkeypatch.setattr(pu.sys, "argv", ["preview_up.py"])
assert pu.main() == 0
assert spawned_port(pu.calls) == "4174"
assert not any(isinstance(c, tuple) and c[0] == "free" for c in pu.calls)
def test_port_arg(pu, monkeypatch):
monkeypatch.setattr(pu.sys, "argv", ["preview_up.py", "--port", "4999"])
assert pu.main() == 0
assert spawned_port(pu.calls) == "4999"
def test_replace_dev_takes_dev_port_after_build(pu, monkeypatch):
monkeypatch.setattr(pu.sys, "argv", ["preview_up.py", "--replace-dev"])
assert pu.main() == 0
assert spawned_port(pu.calls) == "5174"
assert pu.calls.index(("free", 5174)) < next(
i for i, c in enumerate(pu.calls) if isinstance(c, tuple) and c[0] == "spawn"
)
def test_replace_dev_build_fail_kills_nothing(pu, monkeypatch):
def boom(cmd, env):
raise subprocess.CalledProcessError(1, cmd)
monkeypatch.setattr(pu, "run", boom)
monkeypatch.setattr(pu.sys, "argv", ["preview_up.py", "--replace-dev"])
assert pu.main() == 1
assert pu.calls == []
+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))