feat(b07): 안 연 측점 횡단배수 미리 넣음 — 파이썬 짝 (260-4)

- 새 B07_DesignDetail_Engine_Cad_CulvertNote = TS culvertNoteEntities · stationNoLabel 짝(두 파일 머리 표시)
- 횡단 장 만들 때 CAD 칸 없음 · 옛 글 없음 · 배수관 주인 측점 = 정본 pipe_points 로 미리 넣음 칸을 얹은 사본
- 지운 칸(빈 엔티티) · 옛 글 · 설계 없음 · 이웃 측점 = 그대로
- 시험 test_260_note_cad 거울 9쌍(같은 입력 → 같은 엔티티) · 정본 읽기 · B07 그림

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
This commit is contained in:
2026-10-11 14:16:46 +09:00
co-authored by Claude Opus 5.5
parent 83735340ab
commit 96c9eaed37
5 changed files with 418 additions and 4 deletions
+11 -2
View File
@@ -1,6 +1,6 @@
/* PLAN 260 단위검증 헬퍼 — [상세] 표 위 CAD 칸 · 옛 글 옮김 · 횡단배수 미리 넣음.
* `B06_Section_UI_Cross_Detail_Cad.ts` 를 그 자리에서 트랜스파일해 Node 로 돌린다(CAD · 세션 · 서버 = 흉내).
* 결과는 표준출력에 JSON 한 벌. 확장자 `.cjs` 까닭은 `helper_108_drag_box_hit.cjs` 머리와 같다.
* 결과는 표준출력에 JSON 한 벌 · 인자 `mirror` = 표준입력 입력으로 거울 결과(260-4). 확장자 `.cjs` 까닭은 `helper_108_drag_box_hit.cjs` 머리와 같다.
*/
const fs = require("fs");
const path = require("path");
@@ -108,4 +108,13 @@ const out = {
cad.stationNoLabel(1439.97, 20),
],
};
process.stdout.write(JSON.stringify(out));
// 거울(260-4) — 시험이 표준입력으로 준 같은 입력 [{section, station, options, chainage, interval}]
// → TS 엔티티 · 측점 이름(파이썬 짝 `_Engine_Cad_CulvertNote` 와 대조)
if (process.argv[2] === "mirror") {
const cases = JSON.parse(fs.readFileSync(0, "utf8"));
const mirrored = cases.map((item) => ({
entities: cad.culvertNoteEntities(item.section, item.station, item.options),
station: cad.stationNoLabel(item.chainage, item.interval),
}));
process.stdout.write(JSON.stringify(mirrored));
} else process.stdout.write(JSON.stringify(out));
+157
View File
@@ -294,3 +294,160 @@ def test_CAD_칸이_없으면_옛_글_줄_그대로():
noted = _draw({"structure_note": "L형 측구\n집수정 보강"})
labels = [e["shapeData"]["label"] for e in noted["entities"] if e["type"] == "Text"]
assert labels == ["L형 측구", "집수정 보강"]
# ── 260-4 안 연 측점 = B07 이 파이썬 짝으로 넣음 ─────────────────────────
from B06_Section.B06_Section_Engine_Culvert import _culvert_set, pipe_points_file # noqa: E402
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_CulvertNote import ( # noqa: E402
culvert_note_entities,
load_pipe_notes,
station_no_label,
with_culvert_note,
)
_OPTIONS = {
"pipe_diameter_mm": 1200,
"inlet_type": "기슭막이",
"inlet_revet_form": "돌쌓기(찰)",
"inlet_revet_height_m": 2,
"inlet_revet_length_m": 10,
"outlet_type": "기슭막이",
"outlet_revet_form": "돌쌓기(메)",
"outlet_revet_height_m": 2,
"outlet_revet_length_m": 10,
"chute_add_1_structure": "돌붙임",
"chute_add_1_bond": "찰붙임",
"chute_add_1_area": 10,
}
_NO_CHUTE = {k: v for k, v in _OPTIONS.items() if not k.startswith("chute")}
def _case(options: dict, pipe_at: float, chainage: float, design: dict | None, **spec) -> dict:
culvert = {**_culvert_set(options), "chainage_m": pipe_at, **spec}
return {
"section": {"chainage_m": chainage, "culvert": culvert, "design": design},
"station": station_no_label(chainage, 20.0),
"options": options,
"chainage": chainage,
"interval": 20.0,
}
_MIRROR_CASES = [
_case(_OPTIONS, 1433.0, 1433.0, {"pipe_length_m": 22}),
_case(_NO_CHUTE, 1433.0, 1433.0, {}),
_case(
{
**_NO_CHUTE,
"inlet_type": "집수정",
"inlet_structure": "L",
"inlet_basin_height_m": 1.7,
"inlet_basin_length_m": 2,
},
1433.0,
1433.0,
{"pipe_length_m": 9.5},
),
_case({**_NO_CHUTE, "chute_add_2_structure": "도수로", "chute_add_2_area": ""}, 80, 80, None),
_case({**_OPTIONS, "pipe_diameter_mm": 800, "chute_add_1_area": 12.345}, 12.5, 12.5, {}),
_case({**_OPTIONS, "inlet_revet_form": "콘크리트"}, 440.241, 440.0, {"pipe_length_m": 7}),
_case(_OPTIONS, 1433.0, 1440.0, {"pipe_length_m": 22}),
_case(_OPTIONS, 1433.0, 1433.0, {}, body="ford"),
_case(_OPTIONS, 1433.0, 1433.0, {}, hidden_pipe=True),
]
def _without_id(entities: list[dict]) -> list[dict]:
return [{k: v for k, v in entity.items() if k != "id"} for entity in entities]
def test_거울_TS_와_파이썬_짝은_같은_입력에_같은_엔티티():
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HERE / "helper_260_note_cad.cjs"), "mirror"],
input=json.dumps(_MIRROR_CASES, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
mirrored = json.loads(proc.stdout)
assert len(mirrored) == len(_MIRROR_CASES)
for case, ts in zip(_MIRROR_CASES, mirrored, strict=True):
section = case["section"]
py = culvert_note_entities(
section["chainage_m"],
section["culvert"],
section["design"],
case["station"],
case["options"],
)
assert _without_id(py) == _without_id(ts["entities"]), case["station"]
assert station_no_label(case["chainage"], case["interval"]) == ts["station"]
# 대조가 빈 목록끼리만이 아님 — 주인 측점 여섯은 그림이 있음 · 이웃 · 세월교 · 숨은 관 = 빈값
assert [bool(item["entities"]) for item in mirrored] == [True] * 6 + [False] * 3
labels = [e["shapeData"].get("label") for e in mirrored[4]["entities"]]
assert "Φ800m/m" in labels and "관유출부보강(돌붙임,찰) 12.35m2" in labels
def _write_pipes(root: Path, points: list[dict]) -> None:
path = pipe_points_file(root)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"points": points}, ensure_ascii=False), encoding="utf-8")
def test_정본_배수관만_읽음(tmp_path):
_write_pipes(
tmp_path,
[
{"chainage_m": 1433.0, "lonlat": [0, 0], "facility": "pipe", "options": _OPTIONS},
{"chainage_m": 80.0, "lonlat": [0, 0], "facility": "pipe", "options": {}},
{"chainage_m": 90.0, "lonlat": [0, 0], "facility": "ford_bridge", "options": {}},
],
)
pipes = load_pipe_notes(tmp_path)
assert [pipe[0] for pipe in pipes] == [1433.0] # 관경 안 고름 · 세월교 = 빼
assert pipes[0][1]["chainage_m"] == 1433.0 and pipes[0][2] == _OPTIONS
assert load_pipe_notes(tmp_path / "없음") == []
def test_칸_없음_만_미리_넣음_지운_칸_옛_글_설계없음_이웃은_그대로(tmp_path):
_write_pipes(
tmp_path,
[{"chainage_m": 1433.0, "lonlat": [0, 0], "facility": "pipe", "options": _OPTIONS}],
)
pipes = load_pipe_notes(tmp_path)
design = {"pipe_length_m": 22, "paved": False}
seeded = with_culvert_note(design, 1433.0, pipes, "NO.71+13")
assert seeded is not design and "structure_note_cad" not in design # 저장 설계는 안 바뀜
labels = [e["shapeData"].get("label") for e in seeded["structure_note_cad"]["entities"]]
assert labels[2:] == [
"배수관",
"Φ1200m/m",
"L=22.0m",
"NO.71+13",
"관보호공(유입) 기슭막이(찰,H=2.0) L=10.0m",
"관보호공(유출) 기슭막이(메,H=2.0) L=10.0m",
"관유출부보강(돌붙임,찰) 10m2",
]
cleared = {**design, "structure_note_cad": {"entities": []}}
assert with_culvert_note(cleared, 1433.0, pipes, "x") is cleared
old = {**design, "structure_note": "옛 글"}
assert with_culvert_note(old, 1433.0, pipes, "x") is old
assert with_culvert_note(None, 1433.0, pipes, "x") is None
assert with_culvert_note(design, 1440.0, pipes, "x") is design
def test_B07_은_안_연_측점에_미리_넣음을_표_위에_그림(tmp_path):
_write_pipes(
tmp_path,
[{"chainage_m": 20.2, "lonlat": [0, 0], "facility": "pipe", "options": _OPTIONS}],
)
design = with_culvert_note({"pipe_length_m": 22}, 20.0, load_pipe_notes(tmp_path), "NO.1")
drawn = _draw(design)
labels = [e["shapeData"]["label"] for e in drawn["entities"] if e["type"] == "Text"]
assert "NO.1" in labels and "관유출부보강(돌붙임,찰) 10m2" in labels
plain_top = _table(_draw({"pipe_length_m": 22}))["shapeData"]["origin"]["y"]
drop = plain_top - _table(drawn)["shapeData"]["origin"]["y"]
assert drop == pytest.approx(cross_note_block_height(design)) and drop > 15