걷은 함수(운영 부름 0 · 전수 확인 = 화면 · 서버 · 체인 · 추천 엔진 · B07 · 시험) - common_util/common_util_excavation.py 통째(126 줄 · pipe_trench_width_m · wall_trench_area_m2 · wall_trench_width_m · 서버 = Node excavation.trench) - Engine_Grade_Carry.carry_edits(Node carry_seat 겉) · Route_Save_Parts.seat_route_moved(시험 헬퍼 helper_475_1_moved 로 옮김) - Engine_Tilt.attach_underdrains · Engine_Structures_Wall.attach_wall_structures · load_wall_structures - Engine_Grade_Alignment.tangent_elevation(부름 0) · station_key.dedupe_station_keys · loose_chainage 줄 수 전 → 후: excavation.py 126 → 0 · Grade_Carry 178 → 157 · Save_Parts 289 → 269 · Wall 221 → 197 · Tilt 115 → 112 · station_key 141 → 122 · Alignment 657 → 654 남는 짝(머리 「남는 까닭」 한 줄) — Grade · Grade_Profile · Alignment · CurveLaw(2~4 순위 폴백 · 추천) · NodeEdits · SampleExtend · Router_Design · Ranges · Engine_Design · Areas · cross_berm(DTM 아닌 지표면 · Node 못 부를 때 · B07 · 추천) · Tilt · Wall · Culvert(B04 B07 B08) · route_follow · structure_station · station_key · route_polyline(745 줄 · 까닭 = 등록부 R1 줄) 등록부 — until 칸 걷음(G1 = py 표본기 남김 · 까닭 note) · C6 pair = attach_underdrain_specs · F8 pair null 알려진 차 — golden KNOWN 셋 그대로 나타남(R7 표본 출처 · W0 저장 때 경고 · X2 입력 선 자릿수 · 까닭 고침) · underdrain 알려진 차 까닭 · 511_b5 KNOWN_ARC 그대로 계산 자리 창 — 45 줄 · 서버만 아닌 41(바꿀 수 있음 40 + F9) · 서버만 넷(G3 D1 D2 N1 줄마다 까닭) · 옛 까닭 「짝 모으는 중 · B-4」 글 걷음 test_322_3_flow 빨강 — 헬퍼가 @config/config_frontend 흉내(511-A design_calc_server import) 시험 기준 — 걷은 짝 거울 = 걷기 전 파이썬 값 고정(excavation · station_key) · Node carry_seat · attach_underdrain_specs · wall_specs_from 검증: 고친 대상 시험 97 파일 562 통과 · 21 건너뜀 · typecheck 0 · 700 줄 넘는 새 파일 없음 — 요구 320-4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
192 lines
7.0 KiB
Python
192 lines
7.0 KiB
Python
"""C군 벽 제원 — 파이썬·TS 짝 거울 테스트.
|
|
|
|
`B06_Section_Engine_Structures_Wall`(서버, 저장분 기준)과
|
|
`common_util/common_util_structure_walls.ts`(브라우저, 미저장 목록 기준)가 같은 입력에
|
|
같은 제원을 내는지 대조한다(CLAUDE.md 5장 — 짝을 두면 거울 테스트 필수).
|
|
|
|
TS 는 프로젝트 tsc 로 옮겨 **실제 코드 그대로** 돌린다(배수관 세트 거울 테스트와 같은 방식).
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
|
|
|
STRUCTURES = [
|
|
{
|
|
"structure_id": "abc",
|
|
"type_id": "masonry_wet",
|
|
"placement": "interval",
|
|
"chainage_m": 40.0,
|
|
"start_m": 35.0,
|
|
"end_m": 45.0,
|
|
"options": {"height_m": 2.5, "length_m": 10, "before_m": 5, "after_m": 5},
|
|
},
|
|
{
|
|
"structure_id": None,
|
|
"type_id": "retaining_wall",
|
|
"placement": "interval",
|
|
"chainage_m": 120.0,
|
|
# 뒤집힌 구간 — 두 벌 다 작은 값을 시작으로 정렬해야 한다.
|
|
"start_m": 130.0,
|
|
"end_m": 110.0,
|
|
"options": {"height_m": 3.0, "side": "우", "tiers": 2},
|
|
},
|
|
{
|
|
"structure_id": "no-range",
|
|
"type_id": "soil_guard",
|
|
"placement": "interval",
|
|
"chainage_m": 200.0,
|
|
"start_m": None,
|
|
"end_m": None,
|
|
"options": {},
|
|
},
|
|
{
|
|
# 178-3 「추가 단」 상자 둘(둘째 상자 높이 빈 값 · 글 값은 수 아님)
|
|
"structure_id": "tiers",
|
|
"type_id": "soil_guard",
|
|
"placement": "interval",
|
|
"chainage_m": 260.0,
|
|
"start_m": 250.0,
|
|
"end_m": 270.0,
|
|
"options": {
|
|
"form": "돌망태",
|
|
"height_m": 2.0,
|
|
"tiers": 5,
|
|
"tier_2_form": "",
|
|
"tier_2_height": "1.5",
|
|
"tier_1_form": "콘크리트",
|
|
"tier_1_height": 2.0,
|
|
},
|
|
},
|
|
]
|
|
NAMES = {"masonry_wet": "돌쌓기(찰)", "retaining_wall": "옹벽", "soil_guard": "흙막이"}
|
|
CHAINAGES = [20.0, 40.0, 45.02, 60.0, 115.0, 200.0]
|
|
|
|
_RUNNER = """
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { attachWallSpecs, wallSpecsFrom } from "./common_util_structure_walls.js";
|
|
const input = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
|
const specs = wallSpecsFrom(input.structures, new Map(Object.entries(input.names)));
|
|
const sections = input.chainages.map((chainage_m) => ({ chainage_m }));
|
|
attachWallSpecs(sections, specs);
|
|
writeFileSync(process.argv[3], JSON.stringify({ specs, sections }));
|
|
"""
|
|
|
|
|
|
def _python_result() -> dict:
|
|
from B06_Section.B06_Section_Engine_Structures_Wall import (
|
|
_wall_form,
|
|
tier_extras,
|
|
)
|
|
|
|
specs = []
|
|
for item in STRUCTURES:
|
|
name = NAMES.get(item["type_id"])
|
|
start, end = item["start_m"], item["end_m"]
|
|
if not name or start is None or end is None:
|
|
continue
|
|
options = item["options"] or {}
|
|
extras = tier_extras(item["type_id"], options)
|
|
specs.append(
|
|
{
|
|
"structure_id": item["structure_id"],
|
|
"type_id": item["type_id"],
|
|
"name": name,
|
|
"start_m": float(min(start, end)),
|
|
"end_m": float(max(start, end)),
|
|
"anchor_m": float(item["chainage_m"]),
|
|
# 빈 형태 = 구조물 표 형상칸.form(322-1 · 옛 `_FORM_BY_TYPE`)
|
|
"form": options.get("form") or _wall_form(item["type_id"]),
|
|
"height_m": options.get("height_m"),
|
|
"side": options.get("side"),
|
|
# 기초 축(2026-09-09 신설) — 저장 칸과 같은 글자. 초안 경로에서 빠지면
|
|
# 터파기가 안 그려져 「그림이 값에 안 따라간다」가 된다.
|
|
"foundation": options.get("foundation"),
|
|
# 178-3 단 수 = 1 + 「추가 단」 상자 수 · 상자 없으면 옛 tiers
|
|
"tiers": len(extras) + 1 if extras else options.get("tiers"),
|
|
"extras": extras,
|
|
"lift_m": options.get("lift_m"),
|
|
"shift_m": options.get("shift_m"),
|
|
# 전면 기울기 판정 칸(2026-09-14 B5) — 저장 원본 그대로.
|
|
"face_role": options.get("face_role"),
|
|
"face_slope_ratio": options.get("face_slope_ratio"),
|
|
}
|
|
)
|
|
# 얹기 규칙은 같은 함수를 쓸 수 없으므로(정본 파일을 읽는다) 여기서 같은 규칙으로 흉내낸다.
|
|
sections = [{"chainage_m": value} for value in CHAINAGES]
|
|
for section in sections:
|
|
for spec in specs:
|
|
if spec["start_m"] - 0.02 <= section["chainage_m"] <= spec["end_m"] + 0.02:
|
|
section["revetment"] = spec
|
|
break
|
|
return {"specs": specs, "sections": sections}
|
|
|
|
|
|
def _ts_result(tmp_path: Path) -> dict:
|
|
out = tmp_path / "js"
|
|
subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
[
|
|
"node",
|
|
str(TSC),
|
|
str(PROJECT_ROOT / "common_util" / "common_util_structure_walls.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(_RUNNER, encoding="utf-8")
|
|
payload = tmp_path / "input.json"
|
|
result = tmp_path / "output.json"
|
|
payload.write_text(
|
|
json.dumps({"structures": STRUCTURES, "names": NAMES, "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"))
|
|
|
|
|
|
def test_wall_specs_mirror():
|
|
with tempfile.TemporaryDirectory() as workdir:
|
|
ts = _ts_result(Path(workdir))
|
|
py = _python_result()
|
|
assert len(ts["specs"]) == len(py["specs"]) == 3
|
|
assert ts["specs"] == py["specs"]
|
|
tiered = ts["specs"][2]
|
|
assert tiered["tiers"] == 3
|
|
assert tiered["extras"] == [
|
|
{"form": "콘크리트", "height_m": 2.0},
|
|
{"form": None, "height_m": None},
|
|
]
|
|
assert ts["specs"][1]["extras"] == [] # 상자 없는 옹벽 = 옛 tiers 2 그대로
|
|
assert ts["specs"][1]["tiers"] == 2
|
|
ts_marks = [
|
|
(s["chainage_m"], (s.get("revetment") or {}).get("type_id")) for s in ts["sections"]
|
|
]
|
|
py_marks = [
|
|
(s["chainage_m"], (s.get("revetment") or {}).get("type_id")) for s in py["sections"]
|
|
]
|
|
assert ts_marks == py_marks
|
|
# 구간 밖 측점에는 안 붙는다 / 경계 오차(0.02m) 안은 붙는다.
|
|
assert dict(ts_marks)[20.0] is None
|
|
assert dict(ts_marks)[45.02] == "masonry_wet"
|