Merge remote-tracking branch 'origin/sub_laptop_7' into sub_laptop_8

This commit is contained in:
2026-10-08 12:25:23 +09:00
18 changed files with 434 additions and 98 deletions
@@ -3360,7 +3360,8 @@
"chute_add": "도수로/돌붙임",
"chute_add.structure": "구조",
"chute_add.structure=도수로": "도수로",
"chute_add.structure=돌붙임": "돌붙임"
"chute_add.structure=돌붙임": "돌붙임",
"chute_add.thickness": "두께 (cm)"
}
},
"box_culvert": {
@@ -3422,7 +3423,8 @@
"chute_add": "도수로/돌붙임",
"chute_add.structure": "구조",
"chute_add.structure=도수로": "도수로",
"chute_add.structure=돌붙임": "돌붙임"
"chute_add.structure=돌붙임": "돌붙임",
"chute_add.thickness": "두께 (cm)"
}
},
"ford_pavement": {
@@ -3492,7 +3494,8 @@
"chute_add": "도수로/돌붙임",
"chute_add.structure": "구조",
"chute_add.structure=도수로": "도수로",
"chute_add.structure=돌붙임": "돌붙임"
"chute_add.structure=돌붙임": "돌붙임",
"chute_add.thickness": "두께 (cm)"
}
},
"revetment": {
+84
View File
@@ -0,0 +1,84 @@
/* 90-2 도수로 · 돌붙임 띠 헬퍼 — `appendChuteBand`(B06_Section_UI_Cross_Wall_Hatch.ts)를 그 자리에서
* 트랜스파일해 흉내 DOM 에 그림 · 결과 = 띠 꼭짓점(m) · 돌 수(표준출력 JSON). 짝 = `test_90_chute_band.py`.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const cache = new Map();
function loadTs(file) {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => loadTs(path.join(path.dirname(full), `${request}.ts`));
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
class Node {
constructor(tag) {
this.tag = tag;
this.attrs = {};
this.children = [];
}
setAttribute(k, v) {
this.attrs[k] = String(v);
}
getAttribute(k) {
return this.attrs[k] ?? null;
}
append(...kids) {
this.children.push(...kids);
}
set textContent(v) {
this.text = v;
}
}
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
const { appendChuteBand } = loadTs(
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts"),
);
const SCALE = 100;
const run = (line) => {
const layer = new Node("g");
appendChuteBand(
layer,
[line],
{ structure: "돌붙임", thickness_m: 0.3 },
(o) => o * SCALE,
(e) => -e * SCALE,
);
const band = layer.children.find((k) => k.tag === "polygon");
const group = layer.children.find((k) => k.tag === "g");
const points = band.attrs.points.split(" ").map((p) => {
const [px, py] = p.split(",").map(Number);
return { offset: px / SCALE, elevation: -py / SCALE };
});
return {
points,
stones: group.children.filter((k) => k.tag === "rect").length,
clipped: group.attrs["clip-path"]?.startsWith("url(#") ?? false,
title: band.children[0]?.text ?? "",
};
};
const out = {
// 좌측 유출(+ 바깥) · 성토 1:1.2 → 단 사이 수평 조각 → 다시 성토(흙막이 단 사이 포함)
left: run([
{ offset: 5, elevation: 10 },
{ offset: 7.4, elevation: 8 },
{ offset: 8.4, elevation: 8 },
{ offset: 10.8, elevation: 6 },
]),
// 우측 유출(− 바깥) · 곧은 성토 한 줄
right: run([
{ offset: -5, elevation: 10 },
{ offset: -8.6, elevation: 7 },
]),
};
process.stdout.write(JSON.stringify(out));
@@ -75,7 +75,8 @@ def test_저장_값은_도_와_인스턴스_키(out):
"width": "6",
"height": "0.3",
} # 월류 폭 · 높이 = 옛 키 그대로 읽음(65-5)
assert out["state"] == {"slope": [slope], "chute": [{"structure": "돌붙임"}]}
# 두께(cm · 90-2) — 빈 칸은 빈 값(새 상자만 기본 30)
assert out["state"] == {"slope": [slope], "chute": [{"structure": "돌붙임", "thickness": ""}]}
assert out["written"] == {
"ford_slope_deg": 15,
"ford_width_m": 6,
+2 -1
View File
@@ -64,7 +64,8 @@ def test_유입_유출은_구조와_흙막이_칸만(out):
"before",
"after",
]
assert out[spec]["chute_add"] == [["structure", None, None]]
# 도수로/돌붙임 = 구조 · 두께(cm · 새 상자 30 · 90-2)
assert out[spec]["chute_add"] == [["structure", None, None], ["thickness", None, "30"]]
keys = out[f"{spec}Keys"]
assert keys["prefixes"] == ["soil_add_", "chute_add_"]
# 측벽 높이 · 좌우 · 상하 · 바닥판 · 날개벽 치수 칸 없음
+59
View File
@@ -0,0 +1,59 @@
"""90-2 도수로 · 돌붙임 띠 — 윗면 = 유출 성토사면 선 · 사면 안쪽(아래)으로 두께만큼 · 안에 돌.
화면 TS(`appendChuteBand`)를 그 자리에서 돌린 값(`helper_90_chute_band.cjs`).
"""
import json
import math
import subprocess
from pathlib import Path
import pytest
HERE = Path(__file__).parent
ROOT = HERE.parents[1]
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
)
@pytest.fixture(scope="module")
def out() -> dict:
done = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HERE / "helper_90_chute_band.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
)
assert done.returncode == 0, done.stderr
return json.loads(done.stdout)
def _distance(p: dict, a: dict, b: dict) -> float:
dx, dz = b["offset"] - a["offset"], b["elevation"] - a["elevation"]
cross = (p["offset"] - a["offset"]) * dz - (p["elevation"] - a["elevation"]) * dx
return abs(cross) / math.hypot(dx, dz)
@pytest.mark.parametrize("case", ["left", "right"])
def test_띠_두께는_사면에서_직각으로_0_30m_아래(out: dict, case: str) -> None:
points = out[case]["points"]
n = len(points) // 2
top, bottom = points[:n], points[n:][::-1]
for i, low in enumerate(bottom):
# 꼭짓점마다 붙은 변(앞 · 뒤)까지 직각 거리 = 두께
for j in {max(i - 1, 0), min(i, n - 2)}:
assert _distance(low, top[j], top[j + 1]) == pytest.approx(0.3, abs=1e-6)
assert low["elevation"] < top[i]["elevation"] # 사면 안쪽 = 아래
def test_안에_돌_윗면은_사면선(out: dict) -> None:
for case in out.values():
assert case["stones"] >= 2 and case["clipped"]
assert "돌붙임" in case["title"] and "0.30m" in case["title"]
assert out["left"]["points"][:4] == [
{"offset": 5, "elevation": 10},
{"offset": 7.4, "elevation": 8},
{"offset": 8.4, "elevation": 8},
{"offset": 10.8, "elevation": 6},
]
+15 -11
View File
@@ -13,8 +13,7 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402
APRON_LENGTH_FACTOR,
APRON_THICKNESS_M,
CHUTE_THICKNESS_CM,
MIN_PIPE_COVER_M,
REVET_FACE_SLOPE,
_culvert_set,
@@ -24,10 +23,9 @@ from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402
def test_cross_reference_constants_match_plan():
"""계획서 3-0 표 값 — 토피 0.5m·보호공 길이 2배·두께 0.45m(사용자 확정)·전면 1:0.3."""
"""계획서 3-0 표 값 — 토피 0.5m·전면 1:0.3 · 도수로/돌붙임 빈 두께 30 cm(90-2 사용자 확정)."""
assert MIN_PIPE_COVER_M == 0.5
assert APRON_LENGTH_FACTOR == 2.0
assert APRON_THICKNESS_M == 0.45
assert CHUTE_THICKNESS_CM == 30.0
assert REVET_FACE_SLOPE == 0.3
# 배관은 수평도 가능 — 세트에 경사 강제값을 싣지 않는다(2026-08-20 사용자 확정).
assert "min_slope" not in _culvert_set(None)
@@ -54,9 +52,8 @@ def test_saved_values_drive_members():
assert part["revet_form"] is None
assert part["revet_height_m"] == 2.0
assert part["face_slope"] == REVET_FACE_SLOPE
# 보호공 = 낙차고(2.0) × 2 = 4.0m, 두께 0.45m(사용자 확정).
assert part["apron_length_m"] == 4.0
assert part["apron_thickness_m"] == 0.45
# 옛 보호공(apron) 칸은 걷음 — 유출 보호는 도수로/돌붙임 띠(90-2)
assert "apron_length_m" not in part
def test_basin_inlet_skips_revet_and_apron():
@@ -64,10 +61,9 @@ def test_basin_inlet_skips_revet_and_apron():
spec = _culvert_set({"inlet_type": "집수정", "outlet_type": "기슭막이"})
assert spec["inlet"]["structure"] == "집수정"
assert "revet_height_m" not in spec["inlet"]
assert "apron_length_m" not in spec["inlet"]
# 유출측은 그대로 세트를 갖춘다.
assert spec["outlet"]["structure"] == "기슭막이"
assert spec["outlet"]["apron_length_m"] == 4.0 # 빈 높이 = 관경 기준 최소 2.0 × 2
assert spec["outlet"]["revet_height_m"] == 2.0 # 빈 높이 = 관경 기준 최소
def test_string_diameter_and_custom_height():
@@ -77,7 +73,15 @@ def test_string_diameter_and_custom_height():
)
assert spec["diameter_m"] == 0.8
assert spec["outlet"]["revet_height_m"] == 1.5
assert spec["outlet"]["apron_length_m"] == 3.0 # 1.5 × 2
def test_chute_thickness_cm_to_m():
"""도수로/돌붙임(90-2) — 구조를 고르면 `chute` · 두께 cm → m · 빈 두께 30 cm · 안 고르면 없음."""
assert "chute" not in _culvert_set({"chute_add_1_thickness": 40})
spec = _culvert_set({"chute_add_1_structure": "돌붙임", "chute_add_1_thickness": "45"})
assert spec["chute"] == {"structure": "돌붙임", "thickness_m": 0.45}
blank = _culvert_set({"chute_add_1_structure": "도수로", "chute_add_1_thickness": ""})
assert blank["chute"] == {"structure": "도수로", "thickness_m": 0.3}
def test_attach_only_pipe_facilities(tmp_path):
@@ -57,6 +57,10 @@ POINTS = [
"inlet_type": "흙막이",
"inlet_revet_form": "콘크리트",
"inlet_revet_height_m": 1.5,
"inlet_revet_berm_m": 2,
"outlet_revet_berm_m": 3, # 유출 수평공간 칸 없음(89-4) — 세트에 안 실림
# 도수로/돌붙임(90-2) — 빈 두께 = 30 cm
"chute_add_1_structure": "도수로",
},
},
{
@@ -72,6 +76,8 @@ POINTS = [
"outlet_revet_form": "돌쌓기(찰)",
"outlet_revet_height_m": 3.5,
"outlet_revet_length_m": 10,
"chute_add_2_structure": "돌붙임",
"chute_add_2_thickness": 45,
},
},
{
@@ -199,3 +205,8 @@ def test_ford_box_guard_attached_as_culvert() -> None:
assert box["diameter_m"] == round(b["top_thickness_m"] + 2.5 + b["slab_thickness_m"], 3)
# 흙막이 없는 시설엔 안 얹음
assert "culvert" not in sections[100.0]
# 유입만 수평공간(89-4) · 도수로/돌붙임 두께 cm → m(90-2)
assert ford["inlet"]["revet_berm_m"] == 2 and "revet_berm_m" not in ford["outlet"]
assert sections[60.0]["ford"]["chute"] == {"structure": "도수로", "thickness_m": 0.3}
assert box["chute"] == {"structure": "돌붙임", "thickness_m": 0.45}
assert "chute" not in sections[40.0]["culvert"]