Files
Aislo/resources/tester/test_b06_culvert_sets_mirror.py
T
eomsangdonandClaude Opus 5.5 ffbf772f3c feat(B06): 세월교 · 박스 흙막이 횡단 그림 — 관보호공 흙막이와 같은 벽 · 자리 · 연동 · 경사(73-4)
- 유입 · 유출 구조 「흙막이」면 배관 흙막이와 같은 세트(관 숨김 · body)를 같은 측점 culvert 자리에 함께 얹음(파이썬 _guard_set ↔ TS guardSet 짝 · 거울 시험)
- 자동 자리 = 벽 전면이 관(세월교) · 본체(박스) 끝 · 좌우 x 는 그 자리 기준(본체를 늘리면 벽도 따라감) · 상하 d 는 배관과 같음
- 연동 옆 측점 · 3D 연동 해제 자리는 본체 기준 x 를 절대값으로 넘겨 받음 · 3D 만료 해시에 흙막이 값 포함
- 설계선 트림 = 벽 선 쪽은 흙막이 · 안 선 쪽은 본체 · 벽 선 쪽 본체 성토선은 걷음(두 줄 방지) · 관 연장은 안 실음
- 좌측 폼 → 캐시: 세트 다시 짓기 · 높이 · 형태 · 추가 흙막이 상자 수 = 유출 다단 수
- [연동][경사] 회색 풀림(옆 측점에 벽이 서면 연동 · 구간 있으면 경사) — 배관과 같은 규칙
- 시험: test_73_4_body_guard(벽 전면 = 본체 끝 · 박스 +1 m 따라감 · 연동 옆 측점 벽 · 서버 Node 면적 = 화면) · 거울 시험 흙막이 칸

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
2026-10-07 08:23:48 +09:00

202 lines
7.5 KiB
Python

"""배수관 세트 거울 테스트 — 파이썬 엔진과 TS 짝이 **같은 값**을 내는지 대조한다.
짝: `B06_Section/B06_Section_Engine_Culvert.py` ↔ `common_util/common_util_culvert_sets.ts`
한쪽만 고치면 화면과 저장본이 갈리므로, 다섯 시설을 모두 태워 딕셔너리째 비교한다.
레지스트리 기본값은 파이썬이 읽은 것을 그대로 TS 에 넘긴다 — 출처가 같아야 기본값
차이가 아니라 **산식 차이**만 잡힌다.
"""
import json
import subprocess
import sys
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402
attach_culvert_sets,
pipe_points_file,
)
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
# 다섯 시설 × 기본값 폴백/명시값 섞기. 물넘이포장은 폭 절반이 옆 측점까지 걸친다.
POINTS = [
{"chainage_m": 20.0, "facility": "pipe", "options": {"pipe_diameter_mm": 1000}},
{
"chainage_m": 40.0,
"facility": "pipe",
"options": {
"pipe_diameter_mm": "800",
"pipe_kind": "흄관",
"inlet_type": "집수정",
"inlet_basin_length_m": 2.5,
"outlet_revet_height_m": 1.8,
"outlet_revet_length_m": 7.5,
"outlet_revet_form": "돌쌓기",
},
},
{
"chainage_m": 60.0,
"facility": "ford_bridge",
"options": {
"ford_width_m": 12.0,
"ford_height_m": 0.4,
"pipe_count": 2,
"wing_in": "있음",
"wing_in_length_m": 3.0,
"wing_in_angle_deg": 30.0,
"wing_out": "없음",
# 세월교 유입 흙막이(73-4)
"inlet_type": "흙막이",
"inlet_revet_form": "콘크리트",
"inlet_revet_height_m": 1.5,
},
},
{
"chainage_m": 80.0,
"facility": "box_culvert",
"options": {
"body_width_m": 3.0,
"body_height_m": 2.5,
"wing_in": "날개벽",
"wing_out": "흙막이",
"wing_out_length_m": 2.0,
# 박스 유출 흙막이(73-4) — 배관 흙막이와 같은 칸 · `culvert` 자리에 함께 얹힘
"outlet_revet_form": "돌쌓기(찰)",
"outlet_revet_height_m": 3.5,
"outlet_revet_length_m": 10,
},
},
{
"chainage_m": 100.0,
"facility": "ford_pavement",
"options": {"ford_width_m": 9.0, "ford_height_m": 0.25, "ford_slope_pct": 3.0},
},
{
"chainage_m": 120.0,
"facility": "revetment",
"options": {
"side": "좌",
"tiers": 2,
"inlet_revet_height_m": 1.2,
"outlet_revet_length_m": 6.0,
"form": "메쌓기",
},
},
# 물넘이포장 경사각(°) → %(66-2) · 경사 칸이 없으면 기본 3°
{
"chainage_m": 140.0,
"facility": "ford_pavement",
"options": {"ford_height_m": 0.3, "ford_slope_deg": 5},
},
{"chainage_m": 160.0, "facility": "ford_pavement", "options": {"ford_height_m": 0.2}},
]
# 물넘이포장(폭 9m)이 ±4.5m 까지 걸치는지 보려고 95·105 측점을 함께 둔다.
CHAINAGES = [0.0, 20.0, 40.0, 60.0, 80.0, 95.0, 100.0, 105.0, 120.0, 140.0, 160.0]
_TS_RUNNER = """
import { readFileSync, writeFileSync } from "node:fs";
import { attachCulvertSets, buildCulvertSets } from "./common_util_culvert_sets.js";
const input = JSON.parse(readFileSync(process.argv[2], "utf8"));
const sets = buildCulvertSets(input.points, input.registry);
const sections = input.chainages.map((chainage) => ({ chainage_m: chainage }));
attachCulvertSets(sections, sets);
writeFileSync(process.argv[3], JSON.stringify(sections));
"""
def _python_sections() -> list[dict]:
"""파이썬 엔진이 얹은 결과 — 정본 파일을 거쳐 공개 경로로 부른다."""
import tempfile
with tempfile.TemporaryDirectory(prefix="culvert_mirror_") as workdir:
root = Path(workdir)
target = pipe_points_file(root)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(json.dumps({"points": POINTS}, ensure_ascii=False), encoding="utf-8")
sections = [{"chainage_m": value} for value in CHAINAGES]
attach_culvert_sets(root, sections)
return sections
def _registry() -> dict:
types = structure_type_map()
table = {}
for type_id in ("pipe", "ford_bridge", "ford_pavement", "box_culvert"):
entry = types.get(type_id)
table[type_id] = {option.key: option.default for option in entry.options} if entry else {}
return table
def _ts_sections(tmp_path: Path) -> list[dict]:
"""TS 짝을 프로젝트 tsc 로 옮겨 실제 코드를 그대로 돌린다."""
out = tmp_path / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(PROJECT_ROOT / "common_util" / "common_util_culvert_sets.ts"),
"--outDir",
str(out),
"--module",
"esnext",
"--target",
"es2022",
"--moduleResolution",
"bundler",
"--ignoreConfig",
],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
)
(out / "runner.mjs").write_text(_TS_RUNNER, encoding="utf-8")
payload = tmp_path / "input.json"
result = tmp_path / "output.json"
payload.write_text(
json.dumps({"points": POINTS, "registry": _registry(), "chainages": CHAINAGES}),
encoding="utf-8",
)
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(payload), str(result)],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
)
return json.loads(result.read_text(encoding="utf-8"))
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
def test_culvert_sets_match_between_python_and_ts(tmp_path: Path) -> None:
expected = _python_sections()
actual = _ts_sections(tmp_path)
assert len(actual) == len(expected)
for index, (left, right) in enumerate(zip(expected, actual, strict=True)):
assert left == right, f"{CHAINAGES[index]}m 측점 세트가 갈렸다"
# 시설이 실제로 붙었는지 — 빈 결과끼리 같아서 통과하는 것을 막는다.
assert sum(1 for item in expected if len(item) > 1) >= 6
def test_ford_box_guard_attached_as_culvert() -> None:
"""세월교 · 박스 흙막이(73-4) — 흙막이 고른 쪽만 기슭막이 · 관 숨김 · 자리 기준 = 본체."""
sections = {item["chainage_m"]: item for item in _python_sections()}
ford, box = sections[60.0]["culvert"], sections[80.0]["culvert"]
assert (ford["body"], ford["hidden_pipe"], ford["diameter_m"]) == ("ford", True, 1.0)
assert ford["inlet"]["structure"] == "기슭막이" and ford["outlet"]["structure"] == ""
assert ford["inlet"]["revet_form"] == "콘크리트"
assert box["body"] == "box" and box["inlet"]["structure"] == ""
assert box["outlet"]["revet_height_m"] == 3.5
# 본체 전체 높이 = 상판 + 내공 2.5 + 저판
b = sections[80.0]["box"]
assert box["diameter_m"] == round(b["top_thickness_m"] + 2.5 + b["slab_thickness_m"], 3)
# 흙막이 없는 시설엔 안 얹음
assert "culvert" not in sections[100.0]