Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

This commit is contained in:
2026-10-10 15:18:23 +09:00
9 changed files with 298 additions and 35 deletions
@@ -59,13 +59,16 @@ def test_화면_높이_규칙(tmp_path: Path) -> None:
assert run.returncode == 0, run.stderr[-2000:]
got = json.loads(result.read_text(encoding="utf-8"))
for key in ("inlet_revet_height_m", "outlet_revet_height_m", "inlet_basin_height_m"):
for key in ("outlet_revet_height_m", "inlet_basin_height_m"):
assert got["fresh"][key] == pytest.approx(1.7), key # Φ1200 → 1.7
assert got["fresh"]["inlet_revet_height_m"] == pytest.approx(1.9) # 유입 흙막이 + 0.7(211-1)
assert "inlet_revet_height_m" not in got["noPipe"] # 관경 없음 = 안 채움
assert got["read"]["inlet"][0]["height"] == "1.7" # 옛 1.5 · Φ1200 → 1.7
assert got["read"]["outlet"][0]["height"] == "1.6" # 손 값 그대로
assert got["small"]["inlet"][0]["height"] == "1.5" # Φ800 의 1.5 는 그대로
assert got["change"]["inlet"][0]["height"] == "2" # 옛 규칙 값(1.7) → 새 규칙(2.0)
assert (
got["change"]["inlet"][0]["height"] == "2.2"
) # 옛 + 0.5 값(1.7) → 유입 새 규칙(1.5 + 0.7)
assert got["change"]["outlet"][0]["height"] == "2.2" # 손 값 그대로
assert got["old"]["inlet"][0]["height"] == "1.3" # 옛 1.5 → Φ800 규칙
assert got["old"]["inlet"][0]["height"] == "1.5" # 옛 1.5 → Φ800 유입 규칙(0.8 + 0.7)
assert got["old"]["outlet"][0]["height"] == "" # 빈 칸 그대로
@@ -0,0 +1,219 @@
"""211-1 관 보호공 **유입** 흙막이 빈 칸 기본 높이 = 관경(0.1 올림) + 0.7 — 유출 · 집수정은 + 0.5 그대로.
못박는 것
- 파이썬 `pipe_wall_min_height_m(d, "inlet")` · 새 관 기본값(`_apply_new_defaults`) · 관 세트 스펙 · B08 집계 빈 높이.
- TS 짝 `buildCulvertSets` ↔ 파이썬 `_culvert_set`(유입 · 유출 스펙 같은 값) · 횡단도 `revetWallSpec`.
- 저장된 높이는 그대로 · 유출 · 집수정 무변 · 흙막이 아닌 시설(숨은 관)은 본체 높이 그대로.
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
import pytest
from B04_PreProcess import B04_PreProcess_Router_Basins as basins
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B06_Section import B06_Section_Engine_Culvert as culvert
from B08_Quantity import B08_Quantity_Router_Summary as summary
from common_util.common_util_drainage_pipes import PipePoint
from M02_MasterTemplete import M02_Item_Snapshot as snapshots
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
ROOT = Path(__file__).resolve().parents[2]
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
def _revet(side: str, height: float | None = None) -> dict:
found = {
f"{side}_type": "기슭막이",
f"{side}_revet_form": "돌쌓기(메)",
f"{side}_revet_before_m": 5,
f"{side}_revet_after_m": 5,
}
if height is not None:
found[f"{side}_revet_height_m"] = height
return found
def test_파이썬_기본_높이_유입만_0_7():
h = culvert.pipe_wall_min_height_m
assert h(0.8, "inlet") == 1.5 and h(0.8, "outlet") == 1.3
assert h(1.2, "inlet") == 1.9 and h(1.2) == 1.7 # 쪽 안 줌 = 옛 값(집수정)
assert h(1.0, "inlet") == 1.7 and h(1.0, "outlet") == 1.5
assert h(0.45, "inlet") == 1.2 # 0.5 로 올림 + 0.7
def test_새_관_기본값_유입_흙막이만_0_7():
fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 1200})
basins._apply_new_defaults([fresh])
assert fresh.options["inlet_revet_height_m"] == pytest.approx(1.9)
assert fresh.options["outlet_revet_height_m"] == pytest.approx(1.7)
assert fresh.options["inlet_basin_height_m"] == pytest.approx(1.7)
# 이미 적은 높이(추천 · 손 값)는 그대로 · 나머지 칸만 채움
kept = PipePoint(
chainage_m=20.0,
source="stream",
options={"pipe_diameter_mm": 1200, "inlet_revet_height_m": 1.2},
)
basins._apply_new_defaults([kept])
assert kept.options["inlet_revet_height_m"] == 1.2
assert kept.options["outlet_revet_height_m"] == pytest.approx(1.7)
def test_관_세트_빈_높이_유입_0_7_저장값_그대로():
blank = culvert._culvert_set({"pipe_diameter_mm": 1200, **_revet("inlet"), **_revet("outlet")})
assert blank["inlet"]["revet_height_m"] == pytest.approx(1.9)
assert blank["outlet"]["revet_height_m"] == pytest.approx(1.7)
stored = culvert._culvert_set(
{"pipe_diameter_mm": 1200, **_revet("inlet", 1.2), **_revet("outlet", 2.4)}
)
assert stored["inlet"]["revet_height_m"] == 1.2 and stored["outlet"]["revet_height_m"] == 2.4
# 옛 기본 1.5 · 관경 > 1.0 = 저장 값 해석(139-1) — 관경 + 0.5 그대로
legacy = culvert._culvert_set({"pipe_diameter_mm": 1200, **_revet("inlet", 1.5)})
assert legacy["inlet"]["revet_height_m"] == pytest.approx(1.7)
_RUNNER = """
import { readFileSync, writeFileSync } from "node:fs";
import { buildCulvertSets } from "./common_util_culvert_sets.js";
const input = JSON.parse(readFileSync(process.argv[2], "utf8"));
const sets = buildCulvertSets(input.points, input.registry);
writeFileSync(process.argv[3], JSON.stringify([...sets.entries()]));
"""
_WALL_RUNNER = """
import { writeFileSync } from "node:fs";
import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const.js";
const pick = (spec, hidden, auto) => revetWallSpec(spec, {}, hidden, 1.2, auto).pureHeight;
const base = { structure: "기슭막이" };
writeFileSync(process.argv[2], JSON.stringify({
inlet: pick({ ...base, role: "inlet" }, false),
outlet: pick({ ...base, role: "outlet" }, false),
stored: pick({ ...base, role: "inlet", revet_height_m: 1.2 }, false),
hidden: pick({ ...base, role: "inlet" }, true, 2.0),
}));
"""
def _tsc(source: Path, out: Path) -> None:
subprocess.run( # noqa: S603 — 고정 실행 파일 · 타입 오류가 있어도 JS 는 나옴
[
"node",
str(TSC),
str(source),
"--outDir",
str(out),
"--module",
"esnext",
"--target",
"es2022",
"--moduleResolution",
"bundler",
"--skipLibCheck",
"--ignoreConfig",
],
cwd=str(ROOT),
capture_output=True,
)
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
def test_TS_세트_거울(tmp_path: Path) -> None:
points = [
{
"chainage_m": 20.0,
"facility": "pipe",
"options": {"pipe_diameter_mm": 1200, **_revet("inlet"), **_revet("outlet")},
},
{
"chainage_m": 40.0,
"facility": "pipe",
"options": {"pipe_diameter_mm": "800", **_revet("inlet", 1.2), **_revet("outlet")},
},
{
"chainage_m": 60.0,
"facility": "pipe",
"options": {"pipe_diameter_mm": 1200, **_revet("inlet", 1.5)}, # 옛 1.5
},
]
registry = {
type_id: {o.key: o.default for o in entry.options}
for type_id, entry in structure_type_map().items()
if type_id in ("pipe", "ford_bridge", "ford_pavement", "box_culvert")
}
out = tmp_path / "js"
_tsc(ROOT / "common_util" / "common_util_culvert_sets.ts", out)
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
payload, result = tmp_path / "in.json", tmp_path / "out.json"
payload.write_text(json.dumps({"points": points, "registry": registry}), encoding="utf-8")
run = subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(payload), str(result)],
capture_output=True,
text=True,
)
assert run.returncode == 0, run.stderr[-2000:]
got = {at: spec for at, spec in json.loads(result.read_text(encoding="utf-8"))}
for point in points:
want = culvert._culvert_set(point["options"])
spec = got[point["chainage_m"]]
for side in ("inlet", "outlet"):
assert spec[side].get("revet_height_m") == want[side].get("revet_height_m"), point
assert got[20.0]["inlet"]["revet_height_m"] == pytest.approx(1.9)
assert got[20.0]["outlet"]["revet_height_m"] == pytest.approx(1.7)
assert got[40.0]["inlet"]["revet_height_m"] == 1.2 # 저장 값 그대로
assert got[40.0]["outlet"]["revet_height_m"] == pytest.approx(1.3)
assert got[60.0]["inlet"]["revet_height_m"] == pytest.approx(1.7) # 옛 1.5 → 관경 + 0.5
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
def test_횡단도_벽_기본_높이(tmp_path: Path) -> None:
out = tmp_path / "js"
_tsc(ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Const.ts", out)
built = next(
out.rglob("B06_Section_UI_Cross_Culvert_Const.js")
) # 가져온 파일 따라 경로가 깊어짐
runner = built.parent / "runner.mjs"
runner.write_text(_WALL_RUNNER, encoding="utf-8")
result = tmp_path / "out.json"
run = subprocess.run( # noqa: S603
["node", str(runner), str(result)], capture_output=True, text=True
)
assert run.returncode == 0, run.stderr[-2000:]
got = json.loads(result.read_text(encoding="utf-8"))
assert got["inlet"] == pytest.approx(1.9) # Φ1200 + 0.7
assert got["outlet"] == pytest.approx(1.7) # 무변
assert got["stored"] == 1.2 # 칸 값 그대로
assert got["hidden"] == 2.0 # 숨은 관(세월교 · 박스) = 본체 높이 그대로
def test_B08_집계_빈_높이_유입만_0_7(tmp_path: Path) -> None:
combo = {"layer": "system", "owner": None, "key": "SS000011", "name": "횡단배수 배관"}
root = tmp_path / "proj"
edits = root / "B04_PreProcess/drainage/edits"
edits.mkdir(parents=True)
point = {
"chainage_m": 1433.0,
"source": "user",
"options": {"pipe_diameter_mm": 800, **_revet("inlet"), **_revet("outlet")},
"summary_item": combo,
"group_items": {"gnbf9m9": combo, "g9lq9uk": combo},
}
(edits / "pipe_points.json").write_text(
json.dumps({"points": [point]}, ensure_ascii=False), encoding="utf-8"
)
designs = [
{
"chainage_m": 1433.0,
"design": {"pipe_length_m": 18, "inlet_basin": None, "extra_walls": []},
}
]
names = definition.read()["종류"]["pipe"]["이름"]
linked = {owner: {} for owner in snapshots.assignments(root)}
out = summary.pipe_summary(root, designs, names, 6.0, linked=linked)
ids = [c["id"] for c in out["columns"] if c["id"].startswith("pipe:revet")]
assert ids == ["pipe:revet|돌쌓기(메)|1.3", "pipe:revet|돌쌓기(메)|1.5"], (
ids
) # 유출 1.3 · 유입 1.5
@@ -34,8 +34,8 @@ def test_새_관만_기본값_빈_키에만():
fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 800})
basins._apply_new_defaults([fresh])
# 추천(먼저 들어간 값)이 이김 · 집수정 모양 L 도 실제 값(54-39)
# 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · Φ800 → 1.3)
heights = dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.3)
# 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · Φ800 → 1.3) · 유입 흙막이만 + 0.7(211-1 → 1.5)
heights = {**dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.3), "inlet_revet_height_m": 1.5}
expected = {**_defaults("pipe"), "pipe_diameter_mm": 800, "inlet_structure": "L", **heights}
assert fresh.options == expected
assert fresh.options["pipe_kind"] == "파형강관" and fresh.options["inlet_type"] == "집수정"
@@ -66,7 +66,8 @@ def test_원본에_있던_관은_빈칸_그대로(monkeypatch):
by = {p.chainage_m: p for p in points}
assert by[100.0].options is None # 옛 빈칸 그대로
# 새로 보충된 관 = 기본값 + 집수정 모양 L(54-39)
heights = dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.5) # 기본 관경 1000 → 1.5(139-1)
# 기본 관경 1000 → 1.5(139-1) · 유입 흙막이 1.7(211-1)
heights = {**dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.5), "inlet_revet_height_m": 1.7}
assert by[300.0].options == {**_defaults("pipe"), "inlet_structure": "L", **heights}
@@ -26,7 +26,7 @@ def _ts_const(source: str, name: str) -> float:
def test_상수_셋이_TS_와_같다() -> None:
for name in ("REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M"):
for name in ("REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M", "INLET_REVET_ADD_M"):
assert getattr(culvert, name) == _ts_const(TS, name) == _ts_const(SETS, name), name
assert culvert.OLD_PIPE_WALL_DEFAULT_M == _ts_const(SETS, "OLD_PIPE_WALL_DEFAULT_M")
@@ -35,9 +35,16 @@ def test_TS_식_모양이_그대로다() -> None:
"""식을 바꾸면 여기서 잡힘 — 파이썬 식도 함께 고칠 것."""
assert "const raw = diameterM;" in TS and "const raw = diameterM;" in SETS
assert "Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) * REVET_HEIGHT_STEP_M" in TS
assert "return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;" in TS
# 그림이 칸 값을 읽음(139-1) — 배관 기본 = 칸 → 관경 기준
assert "(spec.revet_height_m ?? pipeWallMinPureHeight(diameterM))" in TS
assert (
'revetTargetHeight(diameterM) + (side === "inlet" ? INLET_REVET_ADD_M : REVET_EMBED_DEPTH_M)'
in (
" ".join(TS.split()) # 줄바꿈 · 들여쓰기 무시(포맷터가 접음)
)
.replace("( ", "(")
.replace(" )", ")")
)
# 그림이 칸 값을 읽음(139-1) — 배관 기본 = 칸 → 관경 기준(유입 + 0.7 · 211-1)
assert "(spec.revet_height_m ?? pipeWallMinPureHeight(diameterM, spec.role))" in TS
# 옛 1.5 옮김 — TS 세트 짝 식
assert "Math.abs(stored - OLD_PIPE_WALL_DEFAULT_M) < 1e-9 &&" in SETS
assert "diameterM > 1.0 + 1e-9" in SETS