Files
Aislo/common_util/common_util_node_bundle.py
T
eomsangdonandClaude Opus 5.5 2399c533c6 feat(b06,b07): 표 위 CAD 칸 미리 넣음 = 구조물 조합 이름 + 좌측 패널 옵션 줄 (166-1)
- 구조물마다 조합 줄 뒤에 좌측 패널 옵션 줄 — 폼과 같은 컨테이너 · 이름 · 보이는 칸(`structureOptionLines`) · 구조 갈래에 안 맞는 옛 값 뺌 · 필수 상자 빈 칸 = 폼 기본값
- B06 [상세] 모달 = 화면 TS 직접 · B07 = 같은 TS 를 Node 번들(B07_DesignDetail_Note_Options_Node · build:b07-note)로 받아 파이썬 짝이 붙임 · 실패는 조합 줄만
- 번들 낡음 판정에 A00_Common/design 더함
- 시험 test_166_1(진짜 명세 · B07 = 화면 같은 줄) · test_260 · test_265 기대값

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
2026-10-11 17:55:45 +09:00

117 lines
4.7 KiB
Python

"""브라우저용 TS 를 **서버에서 그대로 실행**하기 위한 공통 배관(2026-09-06 분리).
CLAUDE.md 5장 「계산 자리」 — 같은 계산을 파이썬으로 다시 짜지 않고, 화면이 쓰는 TS 를
Node 진입점으로 감싸 서버가 부른다. 코리도(`B05_Profile_Corridor_Prebuild`)가 첫 사례고
구조물 면적(`B06_Section_Structure_Areas_Prebuild`)이 뒤따르면서, 번들 빌드·실행 배관이
두 벌이 되어 여기로 모았다. **계산은 이 파일에 없다** — 실행 껍데기만 있다.
"""
from __future__ import annotations
import json
import logging
import os
import subprocess
import tempfile
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
ROOT = Path(__file__).resolve().parents[1]
# 번들이 낡았는지 재는 대상 — 기하 계통이 걸쳐 있는 폴더 + 구조물도 식 풀이(B08, 2026-09-13).
SOURCE_DIRS = ("B05_Profile", "B06_Section", "B08_Quantity", "common_util")
# 번들에 박히는 자료(JSON) — 벽 전면 기울기 표준경사 표·기슭막이 치수 표(2026-09-14 B5). 표만 바뀌어도 다시 빌드.
SOURCE_DATA_DIRS = ("resources/data_masonry",)
# 폴더째 훑지 않는 자리 — B07 은 CAD 앱(openwebcad · node_modules)이 함께 살아 맨 위 TS 만 본다 ·
# 구조물 명세(A00 design)는 B07 CAD 칸 옵션 줄 번들이 끌어 씀(166-1).
SOURCE_GLOBS = ("B07_DesignDetail/*.ts", "A00_Common/design/*.ts")
# 번들 만들기·실행 상한(초). 실측 번들 실행 0.1초, 빌드 3초 수준이라 넉넉하다.
BUILD_TIMEOUT_S = 300
RUN_TIMEOUT_S = 600
def node_env() -> dict[str, str]:
"""config/node_modules를 쓰는 프론트엔드 프로세스 환경(main.py와 같은 규약)."""
env = os.environ.copy()
node_modules = ROOT / "config" / "node_modules"
env["PATH"] = f"{node_modules / '.bin'}{os.pathsep}{env.get('PATH', '')}"
env["NODE_PATH"] = str(node_modules)
return env
def bundle_stale(bundle: Path) -> bool:
"""번들이 없거나 TS 원본보다 오래됐으면 참.
번들이 낡으면 서버와 화면이 **다른 값**을 만든다 — 이 판정이 그것을 막는 유일한
장치다. 개발 중에는 `npm run build`를 따로 돌리지 않으므로 여기서 스스로 갱신한다.
"""
if not bundle.is_file():
return True
built_at = bundle.stat().st_mtime
for directory in SOURCE_DIRS:
for path in (ROOT / directory).rglob("*.ts"):
if path.stat().st_mtime > built_at:
return True
for directory in SOURCE_DATA_DIRS:
for path in (ROOT / directory).glob("*.json"):
if path.stat().st_mtime > built_at:
return True
for pattern in SOURCE_GLOBS:
for path in ROOT.glob(pattern):
if path.stat().st_mtime > built_at:
return True
return False
def build_bundle(npm_script: str) -> bool:
result = subprocess.run( # noqa: S602 — 고정 명령, 사용자 입력 없음
f"npm run {npm_script}",
shell=True,
cwd=str(ROOT),
env=node_env(),
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=BUILD_TIMEOUT_S,
)
if result.returncode != 0:
logger.error("Node 번들 빌드 실패(%s):\n%s", npm_script, result.stderr)
return False
return True
def run_node(bundle: Path, input_path: Path, output_path: Path) -> int:
result = subprocess.run( # noqa: S603 — 고정 실행 파일, 인자는 임시 파일 경로뿐
["node", str(bundle), str(input_path), str(output_path)],
cwd=str(ROOT),
env=node_env(),
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=RUN_TIMEOUT_S,
)
if result.returncode != 0:
logger.warning(
"Node 실행 실패(%s, 끝 코드 %s): %s", bundle.name, result.returncode, result.stderr
)
return result.returncode
def run_bundle_json(bundle: Path, npm_script: str, payload: dict[str, Any]) -> Any | None:
"""입력을 JSON 으로 넘겨 실행하고 결과 JSON 을 돌려준다. 실패는 None.
결과가 작을 때만 쓸 것 — 코리도처럼 큰 산출물은 파일로 받아 그대로 옮겨야 한다.
"""
if bundle_stale(bundle) and not build_bundle(npm_script):
return None
with tempfile.TemporaryDirectory(prefix="node_bundle_") as workdir:
source = Path(workdir) / "input.json"
result = Path(workdir) / "output.json"
source.write_text(json.dumps(payload, default=float), encoding="utf-8")
if run_node(bundle, source, result) != 0:
return None
return json.loads(result.read_text(encoding="utf-8"))