"""배수관 연장(m)이 **정본까지 간다** — 2026-09-08 B08 창 요청. 무슨 일이었나 — 관 길이는 횡단 기하가 **m 단위 올림까지** 끝낸 값인데(`CulvertLayout.pipe.lengthM`) **정본(`design`)에 없었다.** 그래서 수량(B08)이 관 11개를 보고도 **연장을 못 내** 배수관 공종이 막혀 있었다. B08 창은 「계산을 서버로 옮기든가 저장 때 남기든가」 둘로 갈래를 냈는데, 실제로는 **그 계산이 이미 서버 Node 로 돌고 있었다**(`B06_Section_Server_Calc_Node` ← `Server_Calc_Prebuild.recompute_server_side`, [저장]·[확정] 때). 그래서 **그 다리에 값 한 줄만** 더 실었다 — 계산을 두 벌로 짜지 않는다(CLAUDE.md 5장). ⚠ 이름은 `pipe_length_m` 로 **못 박았다**. 오늘 `back_len_cm` 을 엔진이 `stone_back_length_cm` 로 읽어 **저장값이 영영 안 닿던** 사고가 있었다 — 키 이름이 어긋나면 값은 조용히 사라진다. """ import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from B06_Section.B06_Section_Schema import CrossSectionPatch # noqa: E402 _LAYOUTS = PROJECT_ROOT / "B06_Section" / "B06_Section_Structure_Layouts.ts" _PERSIST = PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts" _TYPES = PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Types.ts" def test_패치로_받는다(): patch = CrossSectionPatch(chainage_m=85.05, pipe_length_m=7.0) assert patch.pipe_length_m == 7.0 assert CrossSectionPatch(chainage_m=0).pipe_length_m is None def test_음수는_막는다(): import pytest with pytest.raises(ValueError): CrossSectionPatch(chainage_m=0, pipe_length_m=-1) def test_같은_길로_정본에_실린다(): """구조물 면적이 실리는 그 목록에 함께 있어야 [저장]·[확정]·서버 초기값이 모두 나른다.""" source = _LAYOUTS.read_text(encoding="utf-8") assert "export const STRUCTURE_ROW_KEYS" in source block = source[source.index("STRUCTURE_ROW_KEYS = [") :][:300] assert '"pipe_length_m"' in block, "관 길이가 정본에 얹는 키 목록에 없다" assert '"cut_area_m2"' in block, "면적과 같은 목록이어야 한다(길이 하나만 따로 나르지 않는다)" assert "STRUCTURE_ROW_KEYS" in _PERSIST.read_text(encoding="utf-8") def test_면적을_못_내는_측점에서도_길이는_낸다(): """설계선이 모자라 폐회로 면적을 못 내는 자리에도 **관은 서 있다** — 수량은 그 길이가 필요하다.""" source = _LAYOUTS.read_text(encoding="utf-8") assert "return pipeRow;" in source, "면적 실패 시 null 로 떨어지면 관 길이까지 사라진다" assert "layouts.culvert?.pipe?.lengthM" in source def test_이름이_한_벌이다(): """`back_len_cm` ↔ `stone_back_length_cm` 사고 재발 방지 — 이름을 적는 자리가 갈리면 안 된다. 저장 흐름(`_PERSIST`)은 이름을 **직접 안 적고** 공용 목록(`STRUCTURE_ROW_KEYS`)을 돈다 — 그것이 이름이 갈리지 않게 하는 방법이다. 그래서 여기서는 그 점을 지킨다. """ for path in (_LAYOUTS, _TYPES): assert "pipe_length_m" in path.read_text(encoding="utf-8"), path.name persist = _PERSIST.read_text(encoding="utf-8") assert "pipe_length_m" not in persist, "이름을 또 적으면 갈릴 자리가 하나 더 생긴다" assert "STRUCTURE_ROW_KEYS" in persist def test_서버_화이트리스트가_TS_목록과_짝이다(): """⚠ 서버에도 **받을 키 목록**이 따로 있다(`_AREA_KEYS`). 한쪽만 늘리면 Node 가 값을 내도 거기서 조용히 버려진다 — 관 길이를 더하며 실제로 걸린 자리다(2026-09-08). """ import re from B06_Section.B06_Section_Server_Calc_Prebuild import _AREA_KEYS block = _LAYOUTS.read_text(encoding="utf-8") block = block[block.index("STRUCTURE_ROW_KEYS = [") :] block = block[: block.index("] as const")] ts_keys = set(re.findall(r'"([a-z_0-9]+)"', block)) assert ts_keys == set(_AREA_KEYS), ( f"짝이 갈림 — TS {sorted(ts_keys)} vs 서버 {sorted(_AREA_KEYS)}" ) def test_소유_측점에만_싣는다(): """⚠ 세트는 **폭의 절반까지 옆 측점에도** 붙는다(`attach_culvert_sets`). 그 자리에 길이를 실으면 **같은 관을 두 번** 센다 — 실측에서 관 9개에 값이 10곳 실렸다(2026-09-08). 가리는 열쇠는 스펙에 함께 얹는 `chainage_m`(그 시설이 **놓인** 자리)이다. 파이썬·TS 두 쪽이 같이 얹어야 한다 — 한쪽만 얹으면 세트 거울 시험이 깨진다. ⚠ **주인을 가리는 법이 2026-09-09 에 바뀌었다** — 거리(0.02m)로 자르던 것을 **가장 가까운 측점 하나**로 바꿨다. 관 자리와 측점 자리가 **스냅 때문에 최대 0.5m 어긋나기 때문**이다(관 440.241 → 측점 440.0). 거리로 자르면 그런 관은 주인이 없어 길이가 아무 데도 안 실리고 B08 이 「연장 없음」으로 막는다(실측: 배수관 넷). **하나만 고르는 것**은 그대로라 두 번 세지 않는다. """ layouts = _LAYOUTS.read_text(encoding="utf-8") assert "pipeOwnerChainage" in layouts, "소유 측점을 안 가리고 있다" assert "section.culvert?.chainage_m" in layouts, "관이 놓인 자리를 안 읽고 있다" py = (PROJECT_ROOT / "B06_Section" / "B06_Section_Engine_Culvert.py").read_text(encoding="utf-8") ts = (PROJECT_ROOT / "common_util" / "common_util_culvert_sets.ts").read_text(encoding="utf-8") assert '"chainage_m": pipe_chainage' in py assert "chainage_m: pipeChainage" in ts