"""PLAN 323-4 형상 얹기 한 벌 — 파이썬 `attach_shapes` = TS `attachShapes`(거울) · 저장 전 그림 = 저장 뒤 그림. - 42종 견본(관 지점 · 구조물 · 정의 기본값) + 빈칸 견본 → 측점마다 그림 자료(관 세트 · 벽 · 포장 · 맹암거) · 빈칸 경고 `blank`(「종류 칸」 · 측점 계획 자리)가 서버 = 화면. - 옛 열쇠는 걷고 다시(물넘이포장을 옮기면 옛 자리 파임이 빠짐 · 걸친 측점에 새로). - 3D — C군 벽은 덮는 측점 중 기준 측점에서만 시작 ~ 끝 스윕 · 지문 = 그림 자료 통째(손 해시가 빠뜨린 칸도). TS 쪽 = `helper_323_4_attach.cjs`(Node). """ from __future__ import annotations import copy import json import subprocess import tempfile from pathlib import Path import pytest from B05_Profile.B05_Profile_Structures_Schema import StructureInstance, load_definition from B06_Section.B06_Section_Engine_Attach import attach_shapes from common_util.common_util_drainage_pipes import parse_pipe_points from common_util.common_util_structure_catalog import catalog from M02_MasterTemplete.StructOptions.M02_SO_Options import with_visible HERE = Path(__file__).parent ROOT = HERE.parent.parent pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치" ) TYPES = with_visible().model_dump()["types"] def _defaults(type_id: str) -> dict: """정의 기본값(빈 값 뺌) — 그림이 서는 견본.""" options = next(t for t in TYPES if t["type_id"] == type_id)["options"] return {o["key"]: o["default"] for o in options if o["default"] not in (None, "")} def _samples() -> tuple[list[dict], list[dict]]: """42종 하나씩(30 m 간격 · 구간형 = 기준 ± 5) + 빈칸 셋(관경 · 옹벽 높이 · 맹암거 높이).""" pipes: list[dict] = [] structures: list[dict] = [] rows = sorted(catalog().values(), key=lambda row: row["typeId"]) for index, row in enumerate(rows): at = 15.0 + 30.0 * index options = _defaults(row["typeId"]) if row["owner"] == "pipe_points": pipes.append({"chainage_m": at, "facility": row["typeId"], "options": options}) continue span = {"start_m": at - 5, "end_m": at + 5} if row["placement"] == "interval" else {} structures.append( { "structure_id": f"s{index}", "type_id": row["typeId"], "placement": row["placement"], "chainage_m": at, "options": options, **span, } ) end = 15.0 + 30.0 * len(rows) pipes.append({"chainage_m": end, "facility": "pipe", "options": {"pipe_diameter_mm": ""}}) structures.append( { "structure_id": "blank-wall", "type_id": "retaining_wall", "placement": "interval", "start_m": end + 25, "chainage_m": end + 30, "end_m": end + 35, "options": {}, } ) structures.append( { "structure_id": "blank-drain", "type_id": "cross_underdrain", "placement": "point", "chainage_m": end + 60, "options": {}, } ) return pipes, structures def _sections(until: float) -> list[dict]: """5 m 마다 측점(모든 기준 · 시작 · 끝 자리) — 옛 열쇠를 미리 얹어 걷힘도 봄.""" out = [] for step in range(int(until // 5) + 2): section = {"chainage_m": float(step * 5)} if step == 1: section["ford_pavement"] = {"type": "ford_pavement", "chainage_m": 5.0} # 옛 자리 out.append(section) return out PIPES, STRUCTURES = _samples() UNTIL = 15.0 + 30.0 * len(catalog()) + 60 CASES = { "all": {"sections": _sections(UNTIL), "pipes": PIPES, "structures": STRUCTURES}, "empty": {"sections": _sections(30), "pipes": [], "structures": []}, } @pytest.fixture(scope="module") def ts() -> dict: payload = {"definition": load_definition(), "types": TYPES, "cases": CASES} with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "input.json" path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") out = subprocess.run( ["node", str(HERE / "helper_323_4_attach.cjs"), str(path)], capture_output=True, text=True, encoding="utf-8", timeout=120, check=True, ) return json.loads(out.stdout) def _py(case: dict) -> list[dict]: sections = copy.deepcopy(case["sections"]) pipes = parse_pipe_points(case["pipes"]) structures = [StructureInstance.model_validate(s) for s in case["structures"]] attach_shapes(sections, pipes, structures) return json.loads(json.dumps(sections)) def test_mirror_python_equals_ts(ts: dict) -> None: for name, case in CASES.items(): py, js = _py(case), ts["cases"][name] assert len(py) == len(js), name for left, right in zip(py, js): assert left == right, (name, left["chainage_m"]) def test_every_shape_lands(ts: dict) -> None: keys = {key for section in ts["cases"]["all"] for key in section if key != "chainage_m"} assert { "culvert", "ford", "box", "ford_pavement", "revetment", "pavement", "underdrain", } <= keys old = ts["cases"]["all"][1] # 옛 물넘이 자리(5 m) — 걷힘 assert "ford_pavement" not in old assert all(len(section) == 1 for section in ts["cases"]["empty"]) def test_blank_chips(ts: dict) -> None: end = 15.0 + 30.0 * len(catalog()) by_at = {s["chainage_m"]: s.get("blank") for s in ts["cases"]["all"]} assert by_at[end] == ["배관 관경"] assert by_at[end + 25] == by_at[end + 30] == by_at[end + 35] == ["옹벽 높이"] assert by_at[end + 60] == ["맹암거 암거 높이"] assert by_at[end + 5] is None # 계획 자리만 def test_wall_sweeps_once(ts: dict) -> None: assert ts["sweep"] == [None, None, {"beforeM": 8, "afterM": 12}, None] def test_fingerprint_covers_drawing(ts: dict) -> None: assert ts["fingerprint"] == { "same": True, "foundation": True, "pavement": True, "underdrain": True, "adjust": True, "plain": True, }