Files
Aislo/resources/tester/test_b06_pipe_length.py
T
eomsangdonandClaude Opus 5.5 70665bba12 feat(B08): 수리 판정 하상경사 ← B06 관 경사 · 박스 원지반 경사 연결값(110-1)
- B06 정본 키 둘(pipe_slope_pct · box_ground_slope_pct) — 관 길이와 같은 길(STRUCTURE_ROW_KEYS ↔ _AREA_KEYS ↔ CrossSectionPatch) · 주인 측점에만
- B08 수리 표 linked 에 slope_denominator = 100 / 경사 · 0 % 이하 · 짝 없음은 빈칸 · 세월교 · 물넘이는 장 기본값 그대로
- 수리 표 원천 표 「B06」 · 96 「하상경사 안 이음」 주석 고침 · 시험 test_110 새로 · 짝 시험 키 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
2026-10-08 16:14:56 +09:00

126 lines
6.6 KiB
Python

"""배수관 연장(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" in source and "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 "ownerChainage" in layouts, "소유 측점을 안 가리고 있다"
assert "isOwner(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
def test_집수정_실제_모양이_같은_길로_정본에_실린다():
"""98-2 — 실제로 선 집수정 모양(patch · 자동 지형 규칙 결과)이 다단 벽과 같은 줄로 정본에 감.
TS 가 줄에 싣고(`ExtraWallRow.inlet_basin`) 서버가 합칠 때 받아야 함 — 한쪽만 있으면 조용히 버려짐.
"""
layouts = _LAYOUTS.read_text(encoding="utf-8")
block = layouts[layouts.index("export interface ExtraWallRow") :]
assert "inlet_basin: BasinShape | null" in block[: block.index("}")]
assert "inlet_basin: culvert.basin?.shape ?? null" in layouts
prebuild = (PROJECT_ROOT / "B06_Section" / "B06_Section_Server_Calc_Prebuild.py").read_text(
encoding="utf-8"
)
merged = prebuild[prebuild.index("extra_walls = [") :][:900]
assert '"inlet_basin": row.get("inlet_basin")' in merged