"""B05 시설 [저장]이 폼이 모르는 칸을 안 지우는가 (2026-09-14 브레인 판정). ⚠ 앞서 저장이 옵션을 통째로 갈아 끼워, 구조물 집계표·구조물도로 적은 칸(관 기슭막이 기초 등)이 B05 에서 그 시설을 한 번 더 저장하면 조용히 지워졌음 — 되돌릴 길이 없는 사고. 짝: `B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts` — 실제 TS 를 옮겨 돌림. """ from __future__ import annotations import json import re import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" MERGE_TS = ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility_Merge.ts" FORM_TS = ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility.ts" RUNNER = """ import { readFileSync, writeFileSync } from "node:fs"; import { FORM_OPTION_KEYS, mergeFacilityOptions } from "./B05_Profile_UI_Drainage_Facility_Merge.js"; const cases = JSON.parse(readFileSync(process.argv[2], "utf8")); const out = cases.map(([previous, next]) => mergeFacilityOptions(previous, next)); writeFileSync(process.argv[3], JSON.stringify({ out, keys: FORM_OPTION_KEYS })); """ PIPE_BEFORE = { "facility": "pipe", "options": { "pipe_diameter_mm": 1000, "inlet_type": "집수정", "inlet_basin_form": "□형(기본형)", "revet_foundation": "기초유", # 집계표로 적은 칸 — 폼에 없음 "inlet_basin_before_m": 1.5, # 횡단(B06)이 적는 칸 — 폼에 없음 }, } # 폼이 다시 저장 — 유입구를 기슭막이로 바꿔 집수정 칸은 안 보냄 PIPE_FORM = { "facility": "pipe", "options": { "pipe_diameter_mm": 800, "inlet_type": "기슭막이", "inlet_revet_form": "돌쌓기(찰)", }, } REVET_BEFORE = {"facility": "revetment", "options": {"side": "양쪽", "back_len_cm": "35"}} TYPE_CHANGE = {"facility": "ford_pavement", "options": {"ford_width_m": 5}} @pytest.fixture(scope="module") def merged(tmp_path_factory) -> dict: if not TSC.is_file(): pytest.skip("프론트엔드 의존성 미설치") out = tmp_path_factory.mktemp("merge_js") subprocess.run( # noqa: S603 — 고정 실행 파일 [ "node", str(TSC), str(MERGE_TS), "--outDir", str(out), "--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler", "--ignoreConfig", ], cwd=str(ROOT), check=True, capture_output=True, ) (out / "runner.mjs").write_text(RUNNER, encoding="utf-8") cases = [ [PIPE_BEFORE, PIPE_FORM], [REVET_BEFORE, {"facility": "revetment", "options": {"side": "좌"}}], [PIPE_BEFORE, TYPE_CHANGE], [None, PIPE_FORM], ] (out / "cases.json").write_text(json.dumps(cases, ensure_ascii=False), encoding="utf-8") subprocess.run( # noqa: S603 ["node", str(out / "runner.mjs"), str(out / "cases.json"), str(out / "result.json")], cwd=str(ROOT), check=True, capture_output=True, ) return json.loads((out / "result.json").read_text(encoding="utf-8")) def test_폼이_모르는_칸은_남고_폼이_아는_칸은_폼대로(merged) -> None: options = merged["out"][0]["options"] assert options["revet_foundation"] == "기초유" # 집계표 값이 살아남음 assert options["inlet_basin_before_m"] == 1.5 assert options["pipe_diameter_mm"] == 800 # 폼 값이 이김 assert "inlet_basin_form" not in options # 폼이 아는 칸을 폼이 비움 → 지움 assert merged["out"][1]["options"] == {"side": "좌", "back_len_cm": "35"} def test_시설_종류를_바꾸면_통째로_갈아_끼움(merged) -> None: assert merged["out"][2] == TYPE_CHANGE assert merged["out"][3] == PIPE_FORM def test_폼이_적는_칸이_모두_목록에_있다(merged) -> None: """폼에 칸을 늘리고 목록에 안 넣으면 여기서 잡힘.""" source = FORM_TS.read_text(encoding="utf-8") body = source[source.index("readOptions() {") :] branches = re.split(r'current === "(\w+)"', body)[1:] assert branches[::2] == ["pipe", "box_culvert", "ford_pavement", "ford_bridge", "revetment"] for facility, block in zip(branches[::2], branches[1::2]): written = set(re.findall(r'putNumber\(options, "(\w+)"', block)) written |= set(re.findall(r"options\.(\w+) =", block)) missing = written - set(merged["keys"][facility]) assert not missing, f"{facility}: 폼이 적는데 목록에 없음 {missing}"