From 8eb1189b4fc01c5da9e227d48c68d801fb26d2a0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 09:10:47 +0900 Subject: [PATCH] =?UTF-8?q?fix(b05,b06,b08):=20=EA=B8=B0=EC=8A=AD=EB=A7=89?= =?UTF-8?q?=EC=9D=B4=20=EB=B2=BD=20=EB=86=92=EC=9D=B4=EB=A5=BC=20=EA=B4=80?= =?UTF-8?q?=EA=B2=BD=20=EA=B8=B0=EC=A4=80=20=ED=95=9C=20=EA=B0=92=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=C2=B7=20=ED=8F=BC=EC=9D=80=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20=EA=B0=92=EC=9C=BC=EB=A1=9C=20=EC=95=88=20?= =?UTF-8?q?=EB=B3=B4=EB=83=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 한 벽을 두고 값이 셋(등록부·표 H2.5 · 구조물도 H2.5 · 횡단도 관경 기준)이던 결함 — 근거 있는 관경 기준으로 맞춤 - 관 벽 높이 안 적으면 관경 + 여유 0.5 를 0.1m 로 올리고 근입 0.5 더함(Ø800 1.8 · Ø1000 2.0 · Ø1500 2.5) - 파이썬(B06 엔진)·TS(세트 스펙·횡단 상수) 거울 시험 · 등록부 기본 2.5 걷고 empty_means - 최소값이라 미확정·금액 밖은 그대로 - B05 벽 칸(관 유입·유출 · 독립 기슭막이)은 비워 두고 회색 제안값 · [제안값 넣기] 누른 때만 값 — 저장 한 번에 미확정이 확정으로 바뀌던 길을 막음 - 전체 시험: 받은 덤프 적재 커밋 몫 2개만 빨강(보고함) Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B05_Profile/B05_Profile_Structure_Types.json | 10 +-- .../B05_Profile_UI_Drainage_Facility.ts | 7 ++- ...B05_Profile_UI_Drainage_Facility_Fields.ts | 63 +++++++++++++------ B06_Section/B06_Section_Engine_Culvert.py | 34 ++++++++-- B08_Quantity/B08_Quantity_Engine_Pipe.py | 39 +++++++++--- common_util/common_util_culvert_sets.ts | 22 +++++-- .../test_b05_structures_registry_policy.py | 6 +- resources/tester/test_b06_culvert_set.py | 14 +++-- .../test_b06_pipe_wall_height_mirror.py | 43 +++++++++++++ .../tester/test_b08_facility_structures.py | 4 +- 10 files changed, 192 insertions(+), 50 deletions(-) create mode 100644 resources/tester/test_b06_pipe_wall_height_mirror.py diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 6362f9c3..9d7772ef 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -134,9 +134,10 @@ "label": "유입 기슭막이 높이", "input": "number", "unit": "m", - "default": 2.5, + "default": null, "required": false, - "phase": "detail" + "phase": "detail", + "empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)" }, { "key": "outlet_type", @@ -186,9 +187,10 @@ "label": "유출 기슭막이 높이", "input": "number", "unit": "m", - "default": 2.5, + "default": null, "required": false, - "phase": "detail" + "phase": "detail", + "empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)" }, { "key": "revet_foundation", diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 409ed2f1..bd9acc19 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -59,9 +59,9 @@ import { WING_IN_KEYS, WING_OUT_KEYS, } from "./B05_Profile_UI_Drainage_Facility_Fields"; +import { pipeWallMinHeightM } from "../common_util/common_util_culvert_sets"; -// 유입구 형식 목록은 700줄 한계로 `_Fields` 로 옮김(2026-09-14) — 부르던 곳은 그대로 여기서. -export type { InletStructureKind }; +export type { InletStructureKind }; // 목록은 700줄 한계로 `_Fields` 로 옮김 — 부르던 곳은 그대로 /** 시설 확장 정보 한 건 — 관 지점(chainage)에 얹힌다. 배관(pipe)도 부속 옵션을 * 가지면 저장한다. */ @@ -235,9 +235,11 @@ export function createFacilityOptionsForm( const basinRows = [ grid(labeled("형식", basinForm), labeled("길이 (m)", stepper(basinLength, 1))), ]; + const height = (): string => String(pipeWallMinHeightM(Number(pipeDiameter.value) / 1000)); const inletRevet = createRevetmentFields(INLET_REVET_KEYS, { ...REVET_COMMON_DEFAULTS, form: "돌쌓기(찰)", + height, }); // 첫 행 = [구조][형태] — 구조에 따라 집수정 형태와 기슭막이 형태가 갈아 끼워진다 // (2026-08-17 사용자 지시 3). @@ -260,6 +262,7 @@ export function createFacilityOptionsForm( const outletRevet = createRevetmentFields(OUTLET_REVET_KEYS, { ...REVET_COMMON_DEFAULTS, form: "돌쌓기(메)", + height, }); outletGroup.body.append( grid(labeled("구조", outletType), outletRevet.formField), diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index 1b3eb9ce..36d3a3a6 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -201,16 +201,20 @@ export interface RevetmentKeys { after?: string; } -/** 부위별 기본 구성 — 형태는 유입 돌쌓기(찰)·유출 돌쌓기(메), 길이 10m·높이 2.5m. */ +/** 부위별 **제안값** — 형태는 유입 돌쌓기(찰)·유출 돌쌓기(메), 길이 10m. 관 벽 높이는 관경 기준 + * (함수로 받아 볼 때마다 셈). + * ⚠ 제안값은 **칸에 채우지 않고 회색 글씨로만** 보임 — 사용자가 적거나 [제안값 넣기]를 누른 때만 + * 값이 됨(2026-09-14 브레인 판정 「기본값을 몰래 확정으로 바꾸지 않는다」). 칸에 채워 두면 저장이 + * 그 값을 적어 **미확정이 저장 한 번에 확정으로 바뀜** — 사용자가 한 일이 없는데 금액이 섬. */ export interface RevetmentDefaults { form: string; length: string; - height: string; + height: string | (() => string); before: string; after: string; } -/** 부위와 무관한 공통 기본 치수 — 길이 10m(전 5·후 5)·높이 2.5m. */ +/** 부위와 무관한 공통 제안 치수 — 길이 10m(전 5·후 5)·높이 2.5m(독립 기슭막이 · 관 벽은 관경 기준). */ export const REVET_COMMON_DEFAULTS = { length: "10", height: "2.5", @@ -284,26 +288,30 @@ export function createRevetmentFields( keys: RevetmentKeys, defaults: RevetmentDefaults, ): RevetmentFields { + // ⚠ 제안값은 **회색 글씨(placeholder)로만** — 칸은 비워 둠(`RevetmentDefaults` 주석). + const suggestHeight = (): string => + typeof defaults.height === "function" ? defaults.height() : defaults.height; const form = document.createElement("select"); - form.replaceChildren(...REVET_FORMS.map((value) => new Option(value, value))); - form.value = defaults.form; - const length = keys.length ? numberInput("0.1") : null; - if (length) length.value = defaults.length; - const height = numberInput("0.1"); - height.value = defaults.height; + form.replaceChildren( + new Option(`제안 ${defaults.form}`, ""), + ...REVET_FORMS.map((value) => new Option(value, value)), + ); + const length = keys.length ? numberInput("0.1", "0", `제안 ${defaults.length}`) : null; + const height = numberInput("0.1", "0", `제안 ${suggestHeight()}`); + // 관 벽 높이 제안은 관경을 따라감 — 칸을 볼 때마다 다시 셈. + height.addEventListener("focus", () => (height.placeholder = `제안 ${suggestHeight()}`)); // 기준측점 전/후(2026-08-23 사용자) — 구조물 패널(D군)과 같은 연동: // 전 + 후 = 길이, 나중에 고친 쪽이 살아남고 반대쪽이 재배분된다. - const before = keys.before && length ? numberInput("0.1") : null; - const after = keys.after && length ? numberInput("0.1") : null; - if (before) before.value = defaults.before; - if (after) after.value = defaults.after; + const before = keys.before && length ? numberInput("0.1", "0", `제안 ${defaults.before}`) : null; + const after = keys.after && length ? numberInput("0.1", "0", `제안 ${defaults.after}`) : null; let lastSplitEdit: "before" | "after" = "before"; const readNumber = (input: HTMLInputElement | null): number => { const value = input ? Number.parseFloat(input.value) : Number.NaN; return Number.isFinite(value) && value >= 0 ? value : 0; }; const syncSplit = (source: "length" | "before" | "after"): void => { - if (!length || !before || !after) return; + // 길이가 비었으면 전/후를 0 으로 채우지 않음 — 빈 칸이 「0 으로 적음」이 되면 안 됨. + if (!length || !before || !after || length.value === "") return; if (source !== "length") lastSplitEdit = source; const total = readNumber(length); const keep = lastSplitEdit === "after" ? after : before; @@ -319,10 +327,25 @@ export function createRevetmentFields( const beforeField = before ? labeled("기준측점 전 (m)", stepper(before, 0.5)) : null; const afterField = after ? labeled("기준측점 후 (m)", stepper(after, 0.5)) : null; + // [제안값 넣기] — **누른 때만** 빈 칸에 제안값을 넣음(적은 칸은 안 건드림) · 층따기 단추와 같은 모양. + const fill = document.createElement("button"); + fill.type = "button"; + fill.className = "b05-structure__suggest"; + fill.textContent = "제안값 넣기"; + fill.addEventListener("click", () => { + if (!form.value) form.value = defaults.form; + if (length && !length.value) length.value = defaults.length; + if (!height.value) height.value = suggestHeight(); + if (before && !before.value) before.value = defaults.before; + if (after && !after.value) after.value = defaults.after; + form.dispatchEvent(new Event("change")); + }); + // 형태는 그룹 첫 행(구조 옆)에 따로 놓이므로 여기 행에서는 뺀다. const rows = [ grid(...[lengthField, heightField].filter((item): item is HTMLLabelElement => item !== null)), ...(beforeField && afterField ? [grid(beforeField, afterField)] : []), + grid(fill), ]; return { @@ -330,13 +353,15 @@ export function createRevetmentFields( rows, lengthInput: length, write(options) { + // 저장된 값만 칸에 — 없으면 비워 두고 제안값은 회색 글씨로(몰래 채우지 않음). const text = (key: string): string => options[key] !== undefined ? String(options[key]) : ""; - form.value = text(keys.form) || defaults.form; - if (length && keys.length) length.value = text(keys.length) || defaults.length; - height.value = text(keys.height) || defaults.height; - if (before && keys.before) before.value = text(keys.before) || defaults.before; - if (after && keys.after) after.value = text(keys.after) || defaults.after; + form.value = text(keys.form); + if (length && keys.length) length.value = text(keys.length); + height.value = text(keys.height); + height.placeholder = `제안 ${suggestHeight()}`; + if (before && keys.before) before.value = text(keys.before); + if (after && keys.after) after.value = text(keys.after); syncSplit("length"); }, read(target) { diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index 17f00ca6..f1a8bcd4 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -74,6 +74,26 @@ APRON_THICKNESS_M = 0.45 # 1:0.3~0.5의 급한 쪽. 지식DB 근거가 있는 값이라 교차 참조 표시가 붙지 않는다. REVET_FACE_SLOPE = 0.3 +# ── 관 벽 최소 높이 — ⚠ TS 짝: `B06_Section_UI_Cross_Culvert_Const.ts` +# (`REVET_FREEBOARD_M`·`REVET_HEIGHT_STEP_M`·`REVET_EMBED_DEPTH_M`·`pipeWallMinPureHeight`). +# 횡단도가 관 벽을 그리는 그 높이 — 구조물 표·구조물도 그림도 이 값을 씀(2026-09-14 브레인 판정 +# 「그림 둘이 같은 벽을 다르게 그리면 결함 · 근거 있는 관경 기준으로 맞춤」). 거울 시험 +# `test_b06_pipe_wall_height_mirror`. +REVET_FREEBOARD_M = 0.5 # 관 상단 ~ 벽 상단 여유고(잠정 · 토피 0.5 와 같은 값) +REVET_HEIGHT_STEP_M = 0.1 # 높이 눈금 +REVET_EMBED_DEPTH_M = 0.5 # 근입 깊이 + + +def pipe_wall_min_height_m(diameter_m: float) -> float: + """관 벽 **순수 높이**(근입 포함) 최소값 — 관경 + 여유고를 0.1m 로 올린 뒤 근입을 더함. + + ⚠ 「최소」다 — 실제 설계값은 더 클 수 있어 이 값으로 선 수량은 미확정으로 둠(금액 밖). + """ + raw = float(diameter_m) + REVET_FREEBOARD_M + steps = math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) + return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3) + + # (2026-08-20 사용자 확정) 배관은 **수평도 가능** — 그림에서 경사를 강제로 만들지 # 않는다. config `DRAINAGE_PIPE_SLOPE_DEG`(10도)는 유효직경 수리계산 전용으로 남는다. @@ -139,7 +159,9 @@ def _number(value: Any, fallback: float | None) -> float | None: return fallback -def _side_spec(options: dict[str, Any], defaults: dict[str, Any], side: str) -> dict[str, Any]: +def _side_spec( + options: dict[str, Any], defaults: dict[str, Any], side: str, diameter_m: float = 1.0 +) -> dict[str, Any]: """유입("inlet")·유출("outlet") 한쪽의 부속 제원. 구조가 집수정이면 기슭막이·보호공을 만들지 않는다 — 집수정 단면은 후속 작업이라 @@ -164,9 +186,10 @@ def _side_spec(options: dict[str, Any], defaults: dict[str, Any], side: str) -> ) return spec + # 높이를 안 적었으면 **횡단도가 그리는 관경 기준 최소 높이**(2026-09-14 · 옛 기본 2.5 걷음). height = _number( options.get(f"{side}_revet_height_m"), - _number(defaults.get(f"{side}_revet_height_m"), None), + _number(defaults.get(f"{side}_revet_height_m"), pipe_wall_min_height_m(diameter_m)), ) length = _number( options.get(f"{side}_revet_length_m"), @@ -208,10 +231,11 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]: values.get("pipe_diameter_mm"), _number(defaults.get("pipe_diameter_mm"), 1000.0) ) kind = values.get("pipe_kind") or defaults.get("pipe_kind") + diameter_m = round((diameter_mm or 1000.0) / 1000.0, 3) return { "type": "pipe", "pipe_kind": str(kind) if kind else None, - "diameter_m": round((diameter_mm or 1000.0) / 1000.0, 3), + "diameter_m": diameter_m, "min_cover_m": MIN_PIPE_COVER_M, # 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다(2026-09-09). 한쪽만 기초유로 # 하는 일이 드물어 폼을 둘로 늘리지 않았다. 실무가 갈라 쓰면 그때 나눌 것. @@ -219,8 +243,8 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]: str(values.get("revet_foundation") or defaults.get("revet_foundation") or "").strip() or None ), - "inlet": _side_spec(values, defaults, "inlet"), - "outlet": _side_spec(values, defaults, "outlet"), + "inlet": _side_spec(values, defaults, "inlet", diameter_m), + "outlet": _side_spec(values, defaults, "outlet", diameter_m), } diff --git a/B08_Quantity/B08_Quantity_Engine_Pipe.py b/B08_Quantity/B08_Quantity_Engine_Pipe.py index 4c4e1722..ddbde4aa 100644 --- a/B08_Quantity/B08_Quantity_Engine_Pipe.py +++ b/B08_Quantity/B08_Quantity_Engine_Pipe.py @@ -208,12 +208,15 @@ REVET_ROLES = ( ("inlet", "유입부 기슭막이", "유입 칸 벽"), ("outlet", "유출부 기슭막이", "유출 칸 벽"), ) -#: ⚠ 「횡단도와 같은 값」이 아님 — B06 은 관 벽 높이를 **관경 기준 최소 높이**로 따로 그림 -#: (`revetWallSpec`), 독립 기슭막이도 저장 높이가 없으면 근입 깊이로 그림. 형태·길이만 같은 기본값. +#: 관 벽 높이를 안 적었으면 **횡단도가 그리는 관경 기준 최소 높이**(B06 `pipe_wall_min_height_m`, +#: TS 짝 `pipeWallMinPureHeight`)로 섬 — 등록부·구조물도·횡단도가 한 값(2026-09-14 브레인 판정). +#: ⚠ 「최소」라 실제 설계값이 더 클 수 있어 **미확정·금액 밖은 그대로**. NOTE_DEFAULT_WALL = ( - "⚠ 시설 지점에 {filled} 을 안 적어 등록부 기본값으로 섰음 — 미확정이라 금액에 안 들어감" - " · 횡단도 벽 높이는 관경 기준으로 따로 그려져 다를 수 있음" + "⚠ 시설 지점에 {filled} 을 안 적어 기본값으로 섰음 — 미확정이라 금액에 안 들어감" ) +#: 꼬리 — 관 벽은 횡단도와 같은 높이 · 독립 기슭막이는 아직 갈림(관경이 없음). +NOTE_WALL_HEIGHT_PIPE = " · 높이는 횡단도와 같은 관경 기준 최소 높이(실제 설계값은 더 클 수 있음)" +NOTE_WALL_HEIGHT_OWN = " · ⚠ 횡단도는 저장 높이가 없으면 근입 0.5m 만 그림 — 표 높이와 다름" #: 금액에서 뺄 까닭(2026-09-14 브레인 판정) — 줄·표·그림은 서되 내역 합에는 안 듦. UNCONFIRMED_WALL = "벽 치수({filled})가 시설 지점에 없어 기본값으로 섰음 — 적으면 금액이 섬" NOTE_SIDE_UNKNOWN = ( @@ -234,9 +237,13 @@ def _num_or_zero(value: Any) -> float: def _revet_values( - options: dict[str, Any], role: str, defaults: dict[str, Any], legacy: bool + options: dict[str, Any], + role: str, + defaults: dict[str, Any], + legacy: bool, + min_height_m: float | None = None, ) -> tuple[dict[str, Any], list[str]]: - """벽 한쪽 제원과 등록부 기본값으로 채운 칸 — B06 세트 스펙(`_side_spec`)과 같은 채움.""" + """벽 한쪽 제원과 기본값으로 채운 칸 — 등록부 기본값 · 관 벽 높이는 관경 기준 최소 높이.""" values: dict[str, Any] = {} filled: list[str] = [] for name, label in REVET_FIELDS: @@ -245,12 +252,23 @@ def _revet_values( value = options.get(name) if _blank(value): value = defaults.get(name if legacy else f"{role}_revet_{name}") + if value is None and name == "height_m" and min_height_m is not None: + value = min_height_m + label = f"{label}(관경 기준)" if label and value is not None: filled.append(f"{label} {value}") values[name] = value return values, filled +def _pipe_diameter_m(options: dict[str, Any], defaults: dict[str, Any]) -> float: + """관경(m) — 저장값, 없으면 등록부 기본(B06 세트 스펙과 같은 채움).""" + raw = options.get("pipe_diameter_mm") + if _blank(raw): + raw = defaults.get("pipe_diameter_mm") + return _num_or_zero(raw) / 1000.0 or 1.0 + + def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]: """계곡 통과 시설(`pipe_points.json`) → 원단위 전개가 읽는 구조물 줄 (A1, 2026-09-14). @@ -260,6 +278,7 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]: ⚠ 집수정·기슭막이 터파기·되메우기는 전개 성분(`destination: earthwork`)이라 토공집계로만 감. """ from B05_Profile.B05_Profile_Structures_Schema import structure_type_map + from B06_Section.B06_Section_Engine_Culvert import pipe_wall_min_height_m from B08_Quantity.B08_Quantity_Engine_StructureSummary import pipe_row_id from B08_Quantity.B08_Quantity_Engine_UnitQuantity import attachments_of, child_row @@ -304,11 +323,15 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]: ) rows.extend(children) walls = [] + min_height = ( + None if legacy else pipe_wall_min_height_m(_pipe_diameter_m(options, wall_defaults)) + ) for role, pipe_label, own_label in roles: - values, filled = _revet_values(options, role, wall_defaults, legacy) + values, filled = _revet_values(options, role, wall_defaults, legacy, min_height) notes = list(inlet_notes) if role == "inlet" else [] if filled: - notes.append(NOTE_DEFAULT_WALL.format(filled=" · ".join(filled))) + tail = NOTE_WALL_HEIGHT_OWN if legacy else NOTE_WALL_HEIGHT_PIPE + notes.append(NOTE_DEFAULT_WALL.format(filled=" · ".join(filled)) + tail) walls.append([role, own_label if legacy else pipe_label, values, notes, False, filled]) side = str(options.get("side") or "") if legacy else "" if side in ("좌", "우"): diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index 83a726e9..9144fa4d 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -46,6 +46,18 @@ export const FORD_WALL_THICKNESS_M = 0.2; export const FORD_DEFAULT_WIDTH_M = 10.0; export const FORD_PAVEMENT_DEFAULT_WIDTH_M = 5.0; export const BOX_COVER_M = 0.5; +/** 관 벽 최소 높이 — 횡단도 `B06_Section_UI_Cross_Culvert_Const.ts` 와 같은 값(거울 시험이 셋을 대조). + * 높이를 안 적은 관 벽은 이 값으로 섬(2026-09-14 브레인 판정 · 옛 등록부 기본 2.5 걷음). */ +export const REVET_FREEBOARD_M = 0.5; +export const REVET_HEIGHT_STEP_M = 0.1; +export const REVET_EMBED_DEPTH_M = 0.5; + +/** 관 벽 순수 높이(근입 포함) 최소값 — 파이썬 `pipe_wall_min_height_m`. */ +export function pipeWallMinHeightM(diameterM: number): number { + const raw = diameterM + REVET_FREEBOARD_M; + const steps = Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9); + return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3); +} /** 시설 종류 — 파이썬 `common_util_drainage_pipes` 의 상수와 같은 문자열. */ const FACILITY_PIPE = "pipe"; @@ -82,6 +94,7 @@ function sideSpec( options: Record, defaults: Record, side: string, + diameterM: number, ): CulvertSetSpec { const structure = text(options[`${side}_type`]) ?? text(defaults[`${side}_type`]) ?? "기슭막이"; const spec: CulvertSetSpec = { role: side, structure }; @@ -100,7 +113,7 @@ function sideSpec( const height = num( options[`${side}_revet_height_m`], - num(defaults[`${side}_revet_height_m`], null), + num(defaults[`${side}_revet_height_m`], pipeWallMinHeightM(diameterM)), ); const length = num( options[`${side}_revet_length_m`], @@ -133,17 +146,18 @@ function culvertSet( ): CulvertSetSpec { const defaults = registry.pipe ?? {}; const diameterMm = num(options.pipe_diameter_mm, num(defaults.pipe_diameter_mm, 1000.0)); + const diameterM = pipeDiameterM(diameterMm); return { type: "pipe", pipe_kind: text(options.pipe_kind) ?? text(defaults.pipe_kind), - diameter_m: pipeDiameterM(diameterMm), + diameter_m: diameterM, min_cover_m: MIN_PIPE_COVER_M, // 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다. 한 공사에서 한쪽만 기초유로 // 하는 일은 드물어 폼을 둘로 늘리지 않았다(2026-09-09). 실무가 갈라 쓰는 것이 // 확인되면 그때 나눌 것. foundation: text(options.revet_foundation) ?? text(defaults.revet_foundation), - inlet: sideSpec(options, defaults, "inlet"), - outlet: sideSpec(options, defaults, "outlet"), + inlet: sideSpec(options, defaults, "inlet", diameterM), + outlet: sideSpec(options, defaults, "outlet", diameterM), }; } diff --git a/resources/tester/test_b05_structures_registry_policy.py b/resources/tester/test_b05_structures_registry_policy.py index 0f5c2879..7b5ff97e 100644 --- a/resources/tester/test_b05_structures_registry_policy.py +++ b/resources/tester/test_b05_structures_registry_policy.py @@ -451,10 +451,12 @@ def test_pipe_accessory_options_defined(): "outlet_pitching_area_m2", ): assert gone not in options, f"pipe.{gone}는 보호공으로 합쳐졌다" - # 기본 치수 — 길이 10m·높이 2.5m. + # 기본 치수 — 길이 10m. 높이는 **기본값 없음**(2026-09-14 브레인 판정): 비우면 관경 기준 최소 + # 높이로 횡단도·구조물도·표가 같은 값을 씀(옛 2.5 는 횡단도가 안 쓰던 근거 없는 값이라 걷음). for side in ("inlet", "outlet"): assert options[f"{side}_revet_length_m"].default == 10 - assert options[f"{side}_revet_height_m"].default == 2.5 + assert options[f"{side}_revet_height_m"].default is None + assert "관경 기준" in options[f"{side}_revet_height_m"].empty_means # 유무·길이·관경·유입 구조 = b05, 형태·치수 제원 = detail. for key in ("inlet_type", "inlet_revet_length_m", "outlet_revet_length_m"): assert options[key].phase == "b05", f"pipe.{key}는 b05 phase" diff --git a/resources/tester/test_b06_culvert_set.py b/resources/tester/test_b06_culvert_set.py index d5bf7c25..4a35b241 100644 --- a/resources/tester/test_b06_culvert_set.py +++ b/resources/tester/test_b06_culvert_set.py @@ -34,7 +34,11 @@ def test_cross_reference_constants_match_plan(): def test_empty_options_fall_back_to_registry_defaults(): - """옵션이 빈 저장분은 레지스트리 기본값(Ø1000·기슭막이 H2.5/L10)으로 채운다.""" + """옵션이 빈 저장분은 레지스트리 기본값(Ø1000·L10)으로 채운다. + + 높이는 2026-09-14 브레인 판정으로 **관경 기준 최소 높이**(Ø1000 → 1.5 + 근입 0.5 = 2.0) — + 횡단도가 그리는 그 높이. 옛 기본 2.5 는 횡단도가 안 쓰던 값이라 걷음. + """ spec = _culvert_set(None) assert spec["type"] == "pipe" assert spec["diameter_m"] == 1.0 @@ -42,11 +46,11 @@ def test_empty_options_fall_back_to_registry_defaults(): for side in ("inlet", "outlet"): part = spec[side] assert part["structure"] == "기슭막이" - assert part["revet_height_m"] == 2.5 + assert part["revet_height_m"] == 2.0 assert part["revet_length_m"] == 10 assert part["face_slope"] == REVET_FACE_SLOPE - # 보호공 = 낙차고(2.5) × 2 = 5.0m, 두께 0.45m(사용자 확정). - assert part["apron_length_m"] == 5.0 + # 보호공 = 낙차고(2.0) × 2 = 4.0m, 두께 0.45m(사용자 확정). + assert part["apron_length_m"] == 4.0 assert part["apron_thickness_m"] == 0.45 # 레지스트리 기본 형태: 유입 찰 / 유출 메. assert spec["inlet"]["revet_form"] == "돌쌓기(찰)" @@ -61,7 +65,7 @@ def test_basin_inlet_skips_revet_and_apron(): assert "apron_length_m" not in spec["inlet"] # 유출측은 그대로 세트를 갖춘다. assert spec["outlet"]["structure"] == "기슭막이" - assert spec["outlet"]["apron_length_m"] == 5.0 + assert spec["outlet"]["apron_length_m"] == 4.0 # 관경 기준 높이 2.0 × 2 def test_string_diameter_and_custom_height(): diff --git a/resources/tester/test_b06_pipe_wall_height_mirror.py b/resources/tester/test_b06_pipe_wall_height_mirror.py new file mode 100644 index 00000000..a41970e0 --- /dev/null +++ b/resources/tester/test_b06_pipe_wall_height_mirror.py @@ -0,0 +1,43 @@ +"""관 벽 최소 높이 거울 — 파이썬(`B06_Section_Engine_Culvert`) ↔ TS(`_Cross_Culvert_Const.ts`). + +⚠ 횡단도(TS)가 그리는 높이와 구조물 표·구조물도 그림(파이썬)이 같은 벽을 다르게 그리면 결함 + (2026-09-14 브레인 판정). 상수 셋이 같고 식이 같으면 같은 값. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from B06_Section import B06_Section_Engine_Culvert as culvert # noqa: E402 + +TS = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Const.ts").read_text(encoding="utf-8") +#: 세트 스펙 TS 짝(`common_util_culvert_sets.ts`)도 같은 상수를 들고 있음 — 셋을 함께 대조. +SETS = (ROOT / "common_util" / "common_util_culvert_sets.ts").read_text(encoding="utf-8") + + +def _ts_const(source: str, name: str) -> float: + return float(re.search(rf"export const {name} = ([\d.]+);", source).group(1)) + + +def test_상수_셋이_TS_와_같다() -> None: + for name in ("REVET_FREEBOARD_M", "REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M"): + assert getattr(culvert, name) == _ts_const(TS, name) == _ts_const(SETS, name), name + + +def test_TS_식_모양이_그대로다() -> None: + """식을 바꾸면 여기서 잡힘 — 파이썬 식도 함께 고칠 것.""" + assert "const raw = diameterM + REVET_FREEBOARD_M;" in TS + 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 + + +def test_관경별_최소_높이() -> None: + assert culvert.pipe_wall_min_height_m(1.0) == 2.0 + assert culvert.pipe_wall_min_height_m(0.8) == 1.8 + assert culvert.pipe_wall_min_height_m(1.2) == 2.2 diff --git a/resources/tester/test_b08_facility_structures.py b/resources/tester/test_b08_facility_structures.py index 6ad5f803..dec80ba1 100644 --- a/resources/tester/test_b08_facility_structures.py +++ b/resources/tester/test_b08_facility_structures.py @@ -34,7 +34,9 @@ def test_관_하나에_기슭막이_둘이_기본값_사유와_함께_서고_관 inlet, outlet = table["structures"] assert inlet["options"]["form"] == "돌쌓기(찰)" and outlet["options"]["form"] == "돌쌓기(메)" assert all(s["components"] and s["length_m"] == 10 for s in table["structures"]) - assert "등록부 기본값으로 섰음" in inlet["notes"][0] + assert "기본값으로 섰음" in inlet["notes"][0] + # 높이는 횡단도와 같은 관경 기준 최소 높이(Ø1000 → 2.0) — 그림 둘·표가 한 값(브레인 판정) + assert inlet["height_m"] == outlet["height_m"] == 2.0 and "관경 기준" in inlet["notes"][0] # 관 줄은 관 연장만 — 기슭막이가 거기 섞이지 않음(두 번 안 셈). pipe = build_rows([point], [{"chainage_m": 100.0, "design": {"pipe_length_m": 8}}]) assert [row["quantity"] for row in pipe["rows"]] == [8.0]