fix(B06): 세월교 · 박스 유출 수평공간 없앰 — 칸 · 기하 · 세트 짝 · 구조물정의 이름 줄(89-4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
2026-10-08 12:07:48 +09:00
co-authored by Claude Opus 5.5
parent f54def5b2b
commit d9bd08a236
9 changed files with 28 additions and 23 deletions
+2 -1
View File
@@ -83,11 +83,12 @@ export const revetKeys = (side: "inlet" | "outlet"): Record<string, string> => (
});
/** 세월교 · 박스 유입 · 유출 흙막이 칸(79-7) — 높이 빈 칸 = 자동(`heightHint` · 횡단도가 셈) ·
* 절토부 앞 수평공간 칸(`{side}_revet_berm_m`)을 더함. */
* 유입만 절토부 앞 수평공간 칸(`inlet_revet_berm_m` · 유출은 없음 · 89-4)을 더함. */
export function bodyGuardItems(side: "inlet" | "outlet", heightHint: string): SpecItem[] {
const items = guardItems({ 흙막이: revetKeys(side) }, undefined, true).map((item) =>
item.id === "height" ? { ...item, default: undefined, hint: heightHint } : item,
);
if (side === "outlet") return items;
const berm: SpecItem = {
id: "berm",
label: "수평공간 (m)",
@@ -43,7 +43,7 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
...revetKeys("inlet"),
...revetKeys("outlet"),
"inlet_revet_berm_m", // 흙막이 절토부 앞 수평공간(79-7)
"outlet_revet_berm_m",
"outlet_revet_berm_m", // 칸 없앰(89-4) — 목록에 남겨 다음 [저장] 때 옛 값을 지움
],
// 물넘이 포장 = 표 03 명세 키(PLAN 52-3 · `design_structure_ford_pavement`) + 월류 폭 · 높이(65-5) — 옛 경사 ·
// 두께 · 길이는 폼에서 빠져 이어 붙임 · 도수로/돌붙임 키는 `FORM_OPTION_PREFIXES`
@@ -61,7 +61,7 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
...revetKeys("inlet"),
...revetKeys("outlet"),
"inlet_revet_berm_m",
"outlet_revet_berm_m",
"outlet_revet_berm_m", // 칸 없앰(89-4) — 목록에 남겨 다음 [저장] 때 옛 값을 지움
],
revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")],
};
+2 -2
View File
@@ -329,8 +329,8 @@ def _guard_set(values: dict[str, Any], body: str, diameter_m: float) -> dict[str
for role, key in zip(("inlet", "outlet"), _GUARD_KEYS[body]):
guarded = str(values.get(key) or "").strip() == "흙막이"
sides[role] = _revet_side(values, role) if guarded else {"role": role, "structure": ""}
if guarded:
# 절토부 앞 수평공간(79-7) — 흙막이 컨테이너 칸 · 빈 값 = 화면 기본값
if guarded and role == "inlet":
# 절토부 앞 수평공간(79-7) — 유입만(유출은 없음 · 89-4) · 빈 값 = 화면 기본값
sides[role]["revet_berm_m"] = _number(values.get(f"{role}_revet_berm_m"), None)
if not any(side["structure"] for side in sides.values()):
return None
@@ -5,7 +5,7 @@
* 집수정 계류측 두 채널의 입력을 짜고 면적용(미확정 단 제외) 결과를 함께 돌려준다.
* ========================================================================== */
import type { CrossSection, CulvertSideSpec } from "./B06_Section_Api_Fetch";
import type { CrossSection } from "./B06_Section_Api_Fetch";
import {
GUARD_CUT_BERM_M,
REVET_EMBED_DEPTH_M,
@@ -85,17 +85,14 @@ export function buildCulvertTiers(input: {
side: culvert?.hidden_pipe ? culvert.inlet.face_side : null,
});
// 세월교 · 박스 흙막이 끝 절토 = 컨테이너 수평공간 뒤 절토 경사(79-7) · 배관은 종전(3도 선).
const cutBerm = (spec: CulvertSideSpec | undefined) =>
const cutBerm = (lengthM: number) =>
culvert?.body
? {
lengthM: Math.max(spec?.revet_berm_m ?? GUARD_CUT_BERM_M, 0),
slopeRatio: section.design?.cut_slope_ratio ?? 1.0,
}
? { lengthM: Math.max(lengthM, 0), slopeRatio: section.design?.cut_slope_ratio ?? 1.0 }
: undefined;
// 성토부선 + 다단 기슭막이(2026-08-22 — 보호공 삭제, 윗면 선만 성토부선으로).
// 유출측과 **집수정 계류측**이 같은 체계를 쓴다: 5m 넘으면 다단을 둘 수 있다.
const outletInput = {
cutBerm: cutBerm(culvert?.outlet),
cutBerm: cutBerm(0), // 유출 수평공간 없음 — 끝이 원지반에 묻히면 바로 절토(89-4)
startBottomElevation: input.outletBottom,
outward: input.outletInfo.outward,
groundAt,
@@ -109,7 +106,7 @@ export function buildCulvertTiers(input: {
// 독립 기슭막이(관 숨김)는 집수정이 없어 이 채널이 **유입측 벽의 성토부선·다단**이다 —
// 유출측(`extras`)과 같은 시작점(관 하단 꼭짓점)·같은 `bextra` 키(2026-08-29 사용자).
const basinInput = {
cutBerm: cutBerm(culvert?.inlet),
cutBerm: cutBerm(culvert?.inlet.revet_berm_m ?? GUARD_CUT_BERM_M),
startBottomElevation: input.basinBottom,
outward: input.inletInfo.outward,
groundAt,
+3 -2
View File
@@ -213,12 +213,13 @@ export function guardSet(
diameterM: number,
): CulvertSetSpec | null {
const [inletKey, outletKey] = GUARD_KEYS[body];
// 절토부 앞 수평공간(79-7) — 흙막이 컨테이너 칸 · 빈 값 = 화면 기본값
const side = (role: string, key: string): CulvertSetSpec =>
String(values[key] ?? "").trim() === "흙막이"
? { ...revetSide(values, role), revet_berm_m: num(values[`${role}_revet_berm_m`], null) }
? revetSide(values, role)
: { role, structure: "" };
const inlet = side("inlet", inletKey);
// 절토부 앞 수평공간(79-7) — 유입만(유출은 없음 · 89-4) · 빈 값 = 화면 기본값
if (inlet.structure) inlet.revet_berm_m = num(values.inlet_revet_berm_m, null);
const outlet = side("outlet", outletKey);
if (!inlet.structure && !outlet.structure) return null;
return {
@@ -3418,7 +3418,6 @@
"outlet.height": "높이 (m)",
"outlet.before": "기준측점 전 (m)",
"outlet.after": "기준측점 후 (m)",
"outlet.berm": "수평공간 (m)",
"soil_add": "흙막이",
"soil_add.form": "형태",
"soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)",
@@ -3489,7 +3488,6 @@
"outlet.height": "높이 (m)",
"outlet.before": "기준측점 전 (m)",
"outlet.after": "기준측점 후 (m)",
"outlet.berm": "수평공간 (m)",
"soil_add": "흙막이",
"soil_add.form": "형태",
"soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)",
+7 -2
View File
@@ -137,9 +137,14 @@ out.boxWing = wingLayout.sides.map((s) => ({
// ⑤ 흙막이 끝 절토 = 수평공간 뒤 절토 경사 — 수평공간 칸 값을 따름
function cutTails(section, list) {
const culvert = computeStoredLayouts(section, list).culvert;
return [...culvert.outletFill.segments, ...culvert.basinFill.segments]
const tagged = (segments, role) => segments.map((g) => ({ ...g, role }));
return [
...tagged(culvert.outletFill.segments, "outlet"),
...tagged(culvert.basinFill.segments, "inlet"),
]
.filter((g) => g.kind === "cut")
.map((g) => ({
role: g.role,
points: g.points.length,
berm: r3(Math.abs(g.points[1].offset - g.points[0].offset)),
flat: r3(g.points[1].elevation - g.points[0].elevation),
@@ -148,7 +153,7 @@ function cutTails(section, list) {
out.cutDefault = [cutTails(at(352.14), sections), cutTails(at(173.087), sections)];
const berm2 = clone(at(352.14));
berm2.culvert.inlet.revet_berm_m = 2;
berm2.culvert.outlet.revet_berm_m = 2;
berm2.culvert.outlet.revet_berm_m = 2; // 유출은 칸이 없어 무시(89-4)
out.cutBerm2 = cutTails(
berm2,
sections.map((s) => (s === at(352.14) ? berm2 : s)),
+3 -2
View File
@@ -35,7 +35,7 @@ GUARD = [
["height", ["흙막이"], None], # 빈 칸 = 본체 끝 높이(79-7 · 횡단도가 셈)
["before", ["흙막이"], "5"],
["after", ["흙막이"], "5"],
["berm", ["흙막이"], "1"], # 절토부 앞 수평공간(79-7)
["berm", ["흙막이"], "1"], # 절토부 앞 수평공간(79-7) — 유입만(89-4)
]
@@ -55,7 +55,8 @@ def out() -> dict:
def test_유입_유출은_구조와_흙막이_칸만(out):
for spec in ("ford", "box"):
for side in ("inlet", "outlet"):
assert out[spec][side] == GUARD, (spec, side)
want = GUARD if side == "inlet" else GUARD[:-1]
assert out[spec][side] == want, (spec, side)
assert [i[0] for i in out[spec]["soil_add"]] == [
"form",
"length",
+4 -2
View File
@@ -79,12 +79,14 @@ def test_박스_날개벽_끝_절성토선(shape: dict) -> None:
def test_흙막이_끝_절토는_수평공간_뒤(shape: dict) -> None:
tails = [tail for group in shape["cutDefault"] for tail in group]
assert tails, "절토 끝선이 없음"
assert any(tail["role"] == "inlet" for tail in tails)
for tail in tails:
assert tail["points"] == 3
assert tail["flat"] == pytest.approx(0, abs=1e-6)
assert tail["berm"] == pytest.approx(1.0, abs=1e-3)
# 유출은 수평공간 없음 — 묻히면 바로 절토(89-4)
assert tail["berm"] == pytest.approx(1.0 if tail["role"] == "inlet" else 0, abs=1e-3)
for tail in shape["cutBerm2"]:
assert tail["berm"] == pytest.approx(2.0, abs=1e-3)
assert tail["berm"] == pytest.approx(2.0 if tail["role"] == "inlet" else 0, abs=1e-3)
def test_유입_수평공간_시작은_본체_바닥(shape: dict) -> None: