diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 013fe8db..c3fbe39f 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -787,6 +787,15 @@ "required": false, "phase": "detail" }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": ["기초유", "기초버림"], + "default": null, + "required": true, + "phase": "detail" + }, { "key": "side", "label": "설치 측", @@ -878,6 +887,15 @@ "required": false, "phase": "detail" }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": ["기초유", "기초버림"], + "default": null, + "required": true, + "phase": "detail" + }, { "key": "side", "label": "설치 측", @@ -1034,6 +1052,15 @@ "required": false, "phase": "detail" }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": ["기초유", "기초버림"], + "default": null, + "required": true, + "phase": "detail" + }, { "key": "side", "label": "설치 측", @@ -1147,6 +1174,15 @@ "required": false, "phase": "detail" }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": ["기초유", "기초버림"], + "default": null, + "required": true, + "phase": "detail" + }, { "key": "length_m", "label": "길이", @@ -1200,6 +1236,15 @@ "required": false, "phase": "detail" }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": ["기초유", "기초버림"], + "default": null, + "required": true, + "phase": "detail" + }, { "key": "area_m2", "label": "면적", @@ -1316,6 +1361,15 @@ "required": false, "phase": "detail" }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": ["기초유", "기초버림"], + "default": null, + "required": true, + "phase": "detail" + }, { "key": "height_m", "label": "높이", diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index 45a7dab6..7fbca2a6 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -233,6 +233,9 @@ export interface CulvertSet { side?: string; /** 독립 기슭막이 다단 요청 수(1이면 단일 벽). */ tiers?: number; + /** 기초 축 — "기초유" | "기초버림"(실무 정본 탭 제목). **비어 있으면 터파기를 안 그린다** + * — 고르기 전에는 근거가 없다(2026-09-09 사용자 확정 ④). */ + foundation?: string | null; } /** 세월교 날개벽 한쪽 — 횡단면엔 안 보이고 바닥판 연장량만 넘긴다. */ diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index 865df085..de563489 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -263,6 +263,10 @@ def _revet_set(options: dict[str, Any] | None) -> dict[str, Any]: "hidden_pipe": True, "side": str(values.get("side") or "양쪽"), "tiers": int(_number(values.get("tiers"), 1.0) or 1), + # 기초 축(기초유/기초버림) — 터파기 그림·물량이 같이 쓴다. 비어 있으면 안 그린다. + "foundation": (str(values["foundation"]).strip() or None) + if values.get("foundation") is not None + else None, "pipe_kind": None, "diameter_m": 0.3, "min_cover_m": 0.0, diff --git a/B06_Section/B06_Section_UI_Cross_Culvert.ts b/B06_Section/B06_Section_UI_Cross_Culvert.ts index 94c24efe..37865f31 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert.ts @@ -23,6 +23,11 @@ import type { PipeEnd, WallLayout, } from "./B06_Section_UI_Cross_Culvert_Geom"; +import { + appendPipeTrench, + appendWallTrench, + foundationChoice, +} from "./B06_Section_UI_Cross_Excavation"; import { appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch"; // 기하 계산 진입점과 공개 상수·타입은 여기서 재수출한다 — B05(최소 토피)와 횡단 뷰가 @@ -162,6 +167,12 @@ export function appendCulvertOverlay( const revetShapes = new Map(); /** 집수정 부재 도형 — 유입 선택 강조에 쓴다(2026-08-22 사용자 ③). */ const basinShapes: SVGPolygonElement[] = []; + // 터파기는 구조물보다 **먼저** 그린다 — 파선이 벽 밑에 깔려야 도면처럼 보인다. + // 저장 제원 `foundation`(기초유/기초버림)이 비어 있으면 그리지 않는다 — 근거가 없다. + const wallFoundation = foundationChoice(culvert.foundation); + for (const { wall } of wallsInOrder) { + appendWallTrench(layer, wall, wallFoundation, x, toDisplayY); + } for (const { wall, key: wallKey } of wallsInOrder) { // 합성 단면(하부 사다리꼴 + 상부 평행사변형) — 상단 배면이 사면선 접점(사용자 ①·②). const revetShape = polygon( @@ -272,6 +283,26 @@ export function appendCulvertOverlay( const inletFinal = pushOut(pipeCorners.inlet, -1); const outletFinal = pushOut(pipeCorners.outlet, 1); + // 관 터파기 — 표값 폭으로 관 위 지반선에서 관 바닥까지 수직(KCS 44 40 10 그림 3.2-1). + if (!hidePipe) { + const invert = Math.min( + pipeCorners.inlet.bottom.elevation, + pipeCorners.outlet.bottom.elevation, + ); + const centerOffset = (pipeCorners.inlet.bottom.offset + pipeCorners.outlet.bottom.offset) / 2; + appendPipeTrench( + layer, + { + centerOffsetM: centerOffset, + invertM: invert, + topM: invert + diameter + culvert.min_cover_m, + diameterMm: Math.round(diameter * 1000), + }, + x, + toDisplayY, + ); + } + const kindLabel = culvert.pipe_kind ? `${culvert.pipe_kind} ` : ""; const wallThickness = pipeWallThicknessM(culvert.pipe_kind, diameter); const tip = diff --git a/B06_Section/B06_Section_UI_Cross_Excavation.ts b/B06_Section/B06_Section_UI_Cross_Excavation.ts new file mode 100644 index 00000000..d7d1120a --- /dev/null +++ b/B06_Section/B06_Section_UI_Cross_Excavation.ts @@ -0,0 +1,119 @@ +/* ============================================================================= + * B06_Section_UI_Cross_Excavation.ts + * 횡단도에 **터파기 선**을 얹는다 — 관·벽 두 갈래(2026-09-09 사용자 확정 ④). + * + * 왜 있나 — 「터파기가 현재 횡단도에서 표현이 안 된다」가 사용자 지적이었다. 수량을 + * 세기 전에 **도면에 그것이 보여야** 한다. + * + * ⚠ 근거의 급이 다르므로 **노티스를 갈라 적는다** — 값은 `common_util_excavation` 한 곳에서 + * 온다(파이썬 짝과 거울 시험으로 묶여 있다). 여기서는 **그리기만** 한다. + * 관 : 법정 시방서 표(KCS 44 40 10 그림 3.2-1) + * 벽 : 법정 근거 없음 — 실무 정본 식(구조도 기슭막이 xls) + * ⚠ **비탈을 주지 않는다 — 수직으로 그린다.** 정본 식에 기울기 항이 없다. + * ========================================================================== */ + +import { + BASIS_PIPE, + BASIS_WALL, + WALL_BLINDING_DEPTH_M, + WALL_BLINDING_WIDTH_M, + WALL_FOUNDATION_DEPTH_M, + WALL_FOUNDATION_WIDTH_M, + WALL_TRENCH_CLEARANCE_M, + pipeTrenchWidthM, +} from "@util/common_util_excavation"; +import type { WallLayout } from "./B06_Section_UI_Cross_Culvert_Types"; + +const SVG_NS = "http://www.w3.org/2000/svg"; + +/** 「기초유」인가 — 저장 제원 `foundation` 의 값. 비어 있으면 **그리지 않는다**(근거 없음). */ +export function foundationChoice(value: unknown): "기초유" | "기초버림" | null { + const text = typeof value === "string" ? value.trim() : ""; + if (text === "기초유") return "기초유"; + if (text === "기초버림") return "기초버림"; + return null; +} + +function trenchPath(layer: SVGElement, corners: Array<[number, number]>, tooltip: string): void { + const line = document.createElementNS(SVG_NS, "polyline"); + line.setAttribute("points", corners.map(([px, py]) => `${px},${py}`).join(" ")); + line.setAttribute("class", "b06-chart__excavation"); + const title = document.createElementNS(SVG_NS, "title"); + title.textContent = tooltip; + line.append(title); + layer.append(line); +} + +/** + * 관 터파기 — **표값 폭**으로 관 위 지반선에서 관 바닥까지 수직으로 판다. + * 표에 없는 관경이면 **아무것도 그리지 않는다**(폭을 지어내지 않는다). + */ +export function appendPipeTrench( + layer: SVGElement, + input: { + /** 관 중심 offset(m). */ + centerOffsetM: number; + /** 관 바닥(invert) 표고. */ + invertM: number; + /** 터파기 윗면 표고 — 관 위 지반(또는 노면)선. */ + topM: number; + diameterMm: number | null | undefined; + }, + x: (offset: number) => number, + toDisplayY: (elevation: number) => number, +): number | null { + const width = pipeTrenchWidthM(input.diameterMm); + if (width === null || !(input.topM > input.invertM)) return null; + const half = width / 2; + const left = input.centerOffsetM - half; + const right = input.centerOffsetM + half; + trenchPath( + layer, + [ + [x(left), toDisplayY(input.topM)], + [x(left), toDisplayY(input.invertM)], + [x(right), toDisplayY(input.invertM)], + [x(right), toDisplayY(input.topM)], + ], + `관 터파기 폭 ${width.toFixed(2)}m · 깊이 ${(input.topM - input.invertM).toFixed(2)}m\n` + + `Φ${Math.round(Number(input.diameterMm))}㎜ — ${BASIS_PIPE}`, + ); + return width; +} + +/** + * 벽 터파기 — 벽 바닥선 아래로 **수직**. 폭은 바닥 폭 + 여유 0.2, 깊이는 기초 몫. + * `foundation` 이 비어 있으면 **그리지 않는다** — 사용자가 고르기 전에는 근거가 없다. + */ +export function appendWallTrench( + layer: SVGElement, + wall: WallLayout, + foundation: "기초유" | "기초버림" | null, + x: (offset: number) => number, + toDisplayY: (elevation: number) => number, +): { widthM: number; depthM: number } | null { + if (foundation === null) return null; + const hasFoundation = foundation === "기초유"; + const depth = hasFoundation ? WALL_FOUNDATION_DEPTH_M : WALL_BLINDING_DEPTH_M; + // 폭: 기초 몫은 정본이 정한 값(0.9 / 0.7), 다만 벽 바닥이 그보다 넓으면 그 폭 + 여유. + const baseWidth = Math.abs(wall.bottomFront.offset - wall.bottomBack.offset); + const minWidth = hasFoundation ? WALL_FOUNDATION_WIDTH_M : WALL_BLINDING_WIDTH_M; + const width = Math.max(minWidth, baseWidth + (hasFoundation ? WALL_TRENCH_CLEARANCE_M : 0)); + const center = (wall.bottomFront.offset + wall.bottomBack.offset) / 2; + const top = Math.max(wall.bottomFront.elevation, wall.bottomBack.elevation); + const bottom = top - depth; + const left = center - width / 2; + const right = center + width / 2; + trenchPath( + layer, + [ + [x(left), toDisplayY(top)], + [x(left), toDisplayY(bottom)], + [x(right), toDisplayY(bottom)], + [x(right), toDisplayY(top)], + ], + `${foundation} 터파기 폭 ${width.toFixed(2)}m · 깊이 ${depth.toFixed(2)}m (수직)\n` + + `품셈에 규정이 없어 실무 관행으로 정한 값 — ${BASIS_WALL}`, + ); + return { widthM: width, depthM: depth }; +} diff --git a/B06_Section/B06_Section_UI_Style_Cross_Areas.css b/B06_Section/B06_Section_UI_Style_Cross_Areas.css index d6728a14..535a53c9 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Areas.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Areas.css @@ -379,3 +379,12 @@ stroke-dasharray: 4 3; stroke-linejoin: round; } + +/* 터파기 선(2026-09-09 사용자 확정 ④) — **파선**으로 그린다. 구조물 실체가 아니라 + 「여기를 판다」는 표시이고, 근거의 급(법정 표 / 실무 관행)은 말풍선에 적힌다. */ +.b06-chart__excavation { + fill: none; + stroke: #8a5c3c; + stroke-dasharray: 4 3; + stroke-width: 1; +} diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index 1b67957e..51e77620 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -174,6 +174,8 @@ function revetSet(values: Record): CulvertSetSpec { hidden_pipe: true, side: String(values.side ?? "양쪽"), tiers: Math.trunc(num(values.tiers, 1.0) || 1), + // 기초 축(기초유/기초버림) — 터파기 그림·물량이 같이 쓴다. 비어 있으면 안 그린다. + foundation: text(values.foundation), pipe_kind: null, diameter_m: 0.3, min_cover_m: 0.0, diff --git a/common_util/common_util_excavation.py b/common_util/common_util_excavation.py new file mode 100644 index 00000000..d5535eee --- /dev/null +++ b/common_util/common_util_excavation.py @@ -0,0 +1,118 @@ +"""터파기 단면 — 관·벽 두 갈래. **근거의 급이 다르므로 갈라 둔다.** + +⚠⚠ TS 짝 파일과 **한 벌**이다 — `common_util/common_util_excavation.ts`. + 거울 시험: `tmp/tests/test_excavation_mirror.py` (고칠 때 같이 돌릴 것). + 화면(B06 횡단도)이 그리고 서버(B08 수량)가 세는 값이라 두 쪽에 같은 수가 있어야 한다. + +① 관 — **법정 시방서에 표가 있다** + `KCS 44 40 10 배수관(23.01) §3.2(4) 그림 3.2-1`. + 「설계도서에 따로 표기되지 않았을 때에는 그림 3.2-1 터파기 기준 폭에 따라야 한다」 + ⇒ 기본값으로 쓰고 사용자가 덮어쓰는 구조가 원문과 맞다. + ⚠ **관경 + 2b 로 계산하지 않는다** — 표가 관 두께·작업여유를 이미 담고 있다 + (Ø800 은 800+2×300=1400 이 아니라 **1600**). + 여유폭 b 는 같은 그림의 값: 보통지반 0.30 m · 연약지반 0.50 m(한쪽). + +② 벽(돌쌓기·기슭막이) — **법정 근거 없음. 실무 정본 식이 있다** + 법령·교본은 「설계도서 지시 폭·기울기·깊이」까지만 정한다 + (지식DB `01_임도/02_상세설계/구조물/구조물_기초_토공.md` §1 — 전수 확인). + 실무 정본 `original/구조물도/기슭막이/04.구조도(기슭막이).xls` 7탭: + + 터파기 = 기초 0.5 × (0.7 + 0.2) × 1.0 = 0.45 (버림만 있는 탭은 0.1 × 0.7 = 0.07) + + 비탈 H × (평균두께 + 0.2) × 1.0 + + ⇒ **여유폭은 「+0.2」**(평균두께에 더함), **기초는 깊이 0.5 · 폭 0.9**. + ⇒ 정본 탭 제목이 **「기초유 / 기초버림」**이다 — 기초를 안 두는 것이 아니라 + **버림으로 얕게 두는 것**이라 뜻이 다르다. 그 축이 곧 **0.45 냐 0.07 이냐**를 가른다. + ⚠ **비탈을 주지 않는다 — 정본이 수직으로 센다.** 기울기 항이 식에 아예 없다. + 임의로 기울기를 주면 그림과 물량이 어긋난다. + +⚠ 값을 지어내지 않는다 — 여기 있는 수는 전부 위 두 출처의 것이고, 사용자가 덮어쓰면 + 그 값이 이긴다. 화면에는 **근거의 급**(법정 표 / 실무 식)을 노티스로 드러낸다. +""" + +from __future__ import annotations + +#: 관경(㎜) → 터파기 폭(㎜). `KCS 44 40 10` 그림 3.2-1 표 그대로. +PIPE_TRENCH_WIDTH_MM: dict[int, int] = { + 300: 700, + 350: 800, + 400: 900, + 450: 1000, + 500: 1100, + 600: 1200, + 700: 1400, + 800: 1600, + 900: 1700, + 1000: 1800, + 1100: 2000, + 1200: 2100, + 1300: 2200, + 1400: 2400, + 1500: 2500, +} + +#: 한쪽 여유폭(m) — 같은 그림. 표에 없는 관경을 사용자가 넣었을 때의 안내용이다. +PIPE_TRENCH_CLEARANCE_NORMAL_M = 0.30 +PIPE_TRENCH_CLEARANCE_SOFT_M = 0.50 + +#: 벽 터파기 — 실무 정본 식의 수(위 문서 참조). +WALL_TRENCH_CLEARANCE_M = 0.2 # 평균두께에 더하는 여유 +WALL_FOUNDATION_DEPTH_M = 0.5 # 기초 깊이 +WALL_FOUNDATION_WIDTH_M = 0.9 # 기초 폭 (0.7 + 0.2) +WALL_BLINDING_DEPTH_M = 0.1 # 기초 없이 버림만 있을 때의 깊이 +WALL_BLINDING_WIDTH_M = 0.7 # 그때의 폭 (여유폭 0.0 — 버림 탭에는 여유가 없다) + +#: 근거의 급 — 화면 노티스가 이 값을 그대로 쓴다. +BASIS_PIPE = "법정 표(KCS 44 40 10 그림 3.2-1) — 설계도서에 따로 없으면 이 값" +BASIS_WALL = "법정 근거 없음 · 실무 정본 식(구조도 기슭막이 xls) 기본값 — 확인 후 쓸 것" + + +def pipe_trench_width_m(diameter_mm: float | int | None) -> float | None: + """관경(㎜)에 대응하는 터파기 폭(m). 표에 없으면 `None`(지어내지 않는다).""" + if diameter_mm is None: + return None + try: + key = int(round(float(diameter_mm))) + except (TypeError, ValueError): + return None + width = PIPE_TRENCH_WIDTH_MM.get(key) + return None if width is None else width / 1000.0 + + +def wall_trench_area_m2( + height_m: float | None, + average_thickness_m: float | None, + *, + has_foundation: bool, +) -> float | None: + """벽 터파기 단면적(㎡/m). 높이나 평균두께가 없으면 `None`. + + 실무 정본 식 그대로: `기초분 + H × (평균두께 + 0.2)`. + `has_foundation=False` 면 기초분이 **버림 몫(0.1 × 0.7)**으로 줄어든다. + """ + if height_m is None or average_thickness_m is None: + return None + try: + height = float(height_m) + thickness = float(average_thickness_m) + except (TypeError, ValueError): + return None + if height <= 0 or thickness <= 0: + return None + base = ( + WALL_FOUNDATION_DEPTH_M * WALL_FOUNDATION_WIDTH_M + if has_foundation + else WALL_BLINDING_DEPTH_M * WALL_BLINDING_WIDTH_M + ) + return base + height * (thickness + WALL_TRENCH_CLEARANCE_M) + + +def wall_trench_width_m(average_thickness_m: float | None) -> float | None: + """벽 터파기 폭(m) — 평균두께 + 여유 0.2. 값이 없으면 `None`.""" + if average_thickness_m is None: + return None + try: + thickness = float(average_thickness_m) + except (TypeError, ValueError): + return None + return None if thickness <= 0 else thickness + WALL_TRENCH_CLEARANCE_M diff --git a/common_util/common_util_excavation.ts b/common_util/common_util_excavation.ts new file mode 100644 index 00000000..5452b7b5 --- /dev/null +++ b/common_util/common_util_excavation.ts @@ -0,0 +1,88 @@ +/* ============================================================================= + * common_util_excavation.ts + * 터파기 단면 — 관·벽 두 갈래. **근거의 급이 다르므로 갈라 둔다.** + * + * ⚠⚠ 파이썬 짝과 **한 벌**이다 — `common_util/common_util_excavation.py`. + * 거울 시험: `tmp/tests/test_excavation_mirror.py`. 어느 쪽을 고치든 같이 돌릴 것. + * 화면(B06 횡단도)이 그리고 서버(B08 수량)가 세는 값이라 두 쪽 수가 같아야 한다. + * + * ① 관 — 법정 시방서에 표가 있다: `KCS 44 40 10 배수관(23.01) §3.2(4) 그림 3.2-1`. + * 「설계도서에 따로 표기되지 않았을 때에는 … 기준 폭에 따라야 한다」 + * ⚠ **관경 + 2b 로 계산하지 않는다** — 표가 관 두께·작업여유를 이미 담고 있다. + * ② 벽 — 법정 근거 없음. 실무 정본 식(`구조도(기슭막이).xls` 7탭): + * 터파기 = 기초 0.5 × 0.9 (기초버림이면 0.1 × 0.7) + H × (평균두께 + 0.2) + * ⚠ **비탈을 주지 않는다 — 정본이 수직으로 센다.** 축 이름은 「기초유 / 기초버림」이다. + * ========================================================================== */ + +/** 관경(㎜) → 터파기 폭(㎜). KCS 44 40 10 그림 3.2-1 표 그대로. */ +export const PIPE_TRENCH_WIDTH_MM: Record = { + 300: 700, + 350: 800, + 400: 900, + 450: 1000, + 500: 1100, + 600: 1200, + 700: 1400, + 800: 1600, + 900: 1700, + 1000: 1800, + 1100: 2000, + 1200: 2100, + 1300: 2200, + 1400: 2400, + 1500: 2500, +}; + +/** 한쪽 여유폭(m) — 같은 그림. 표에 없는 관경을 넣었을 때의 안내용. */ +export const PIPE_TRENCH_CLEARANCE_NORMAL_M = 0.3; +export const PIPE_TRENCH_CLEARANCE_SOFT_M = 0.5; + +/** 벽 터파기 — 실무 정본 식의 수. */ +export const WALL_TRENCH_CLEARANCE_M = 0.2; +export const WALL_FOUNDATION_DEPTH_M = 0.5; +export const WALL_FOUNDATION_WIDTH_M = 0.9; +export const WALL_BLINDING_DEPTH_M = 0.1; +export const WALL_BLINDING_WIDTH_M = 0.7; + +/** 근거의 급 — 화면 노티스가 이 문구를 그대로 쓴다. */ +export const BASIS_PIPE = "법정 표(KCS 44 40 10 그림 3.2-1) — 설계도서에 따로 없으면 이 값"; +export const BASIS_WALL = + "법정 근거 없음 · 실무 정본 식(구조도 기슭막이 xls) 기본값 — 확인 후 쓸 것"; + +/** 관경(㎜)에 대응하는 터파기 폭(m). 표에 없으면 `null`(지어내지 않는다). */ +export function pipeTrenchWidthM(diameterMm: number | null | undefined): number | null { + if (diameterMm === null || diameterMm === undefined) return null; + const key = Math.round(Number(diameterMm)); + if (!Number.isFinite(key)) return null; + const width = PIPE_TRENCH_WIDTH_MM[key]; + return width === undefined ? null : width / 1000; +} + +/** + * 벽 터파기 단면적(㎡/m). 높이·평균두께가 없으면 `null`. + * `hasFoundation=false` 면 기초분이 버림 몫(0.1 × 0.7)으로 줄어든다. + */ +export function wallTrenchAreaM2( + heightM: number | null | undefined, + averageThicknessM: number | null | undefined, + hasFoundation: boolean, +): number | null { + if (heightM === null || heightM === undefined) return null; + if (averageThicknessM === null || averageThicknessM === undefined) return null; + const height = Number(heightM); + const thickness = Number(averageThicknessM); + if (!Number.isFinite(height) || !Number.isFinite(thickness)) return null; + if (height <= 0 || thickness <= 0) return null; + const base = hasFoundation + ? WALL_FOUNDATION_DEPTH_M * WALL_FOUNDATION_WIDTH_M + : WALL_BLINDING_DEPTH_M * WALL_BLINDING_WIDTH_M; + return base + height * (thickness + WALL_TRENCH_CLEARANCE_M); +} + +/** 벽 터파기 폭(m) — 평균두께 + 여유 0.2. 값이 없으면 `null`. */ +export function wallTrenchWidthM(averageThicknessM: number | null | undefined): number | null { + if (averageThicknessM === null || averageThicknessM === undefined) return null; + const thickness = Number(averageThicknessM); + if (!Number.isFinite(thickness) || thickness <= 0) return null; + return thickness + WALL_TRENCH_CLEARANCE_M; +}