From ce7d0125d225267876e4b1f16fb095b8232adf86 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 14:37:10 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(structure):=2054-39=20=C2=B7=2054-40?= =?UTF-8?q?=20=C2=B7=2054-41=20=EC=84=A0=ED=83=9D=20=EC=95=88=20=ED=95=A8?= =?UTF-8?q?=20=3D=20=EA=B0=92=20=EC=97=86=EC=9D=8C=20=C2=B7=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=EC=9D=80=20=EC=B9=B8=EC=97=90=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20=EA=B0=92=20=C2=B7=20=EC=97=B0=EB=8F=99/=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=EB=8A=94=20=ED=9D=99=EB=A7=89=EC=9D=B4=EB=A7=8C=20?= =?UTF-8?q?=C2=B7=20=EB=82=B4=EA=B3=B5=20=EC=B9=B8=20=EC=88=A8=EA=B9=80=20?= =?UTF-8?q?=C2=B7=20=EC=83=81=EC=9E=90=20=EC=95=88=20=EB=8B=A8=EC=B6=94=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4=20=ED=85=9C=ED=94=8C=EB=A6=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 54-39 고르기 칸 빈 보기 「선택 안 함」 · 「(기본)」 꼬리 · 숫자 칸 「(기본)」 자리표시 · 알약 라벨 「(기본)」 걷음 - 54-39 엔진 짝(파이썬 · TS): 배관 구조 · 형태 · 관종 · 기초 · 길이 · 전 · 후 · 집수정 높이 빈 값 = 값 없음(정의 기본값으로 안 읽음) · 관경 빈 값 = 관 없음 · 집수정 모양(basin_form) 실음 - 54-39 횡단 기하: 구조 · 집수정 모양 안 고름 = 집수정 없음 · 구조 · 형태 안 고른 배관 벽 = 그리지 않음 · patch 형식이 없으면 폼 모양 - 54-39 새로 놓기 · 종류 바꾸기 · 자동 배치 = 집수정 모양 L 도 실제 값 - 54-40 [연동] [경사] = 유입 · 유출 구조 흙막이일 때만 · 집수정 「내공」 줄 화면에서 숨김(값 · 그림 그대로) - 54-41 상자 안 단추(연동 · 경사 · 선택 · 해제) 높이 = 숫자 칸 묶음 26 · 상자 밖 단추 그대로 - 시험: 새 규칙 기대값(culvert_set · culvert_sets_mirror · structure_span · new_structure_defaults) 화면 확인(sub5_검증): 새 배관 = 1000 · 집수정 L형 1.5 · 흙막이 찰 10 · 1.5 · 5 · 5 실제 값 · (기본) 0 · 유입 집수정 0 ↔ 흙막이 1 단추 줄 · 횡단 eval 형태 빈 값 = 집수정 · 벽 0 · 상자 안 단추 26 · 상자 밖 35.3 · 내공 줄 0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- A00_Common/design/design_structure_form.ts | 6 +- .../B04_PreProcess_Router_Basins.py | 3 + .../B05_Profile_UI_Drainage_Facility.ts | 65 +++---------------- .../B05_Profile_UI_Structures_Balloon.ts | 29 ++------- .../B05_Profile_UI_Structures_Fields.ts | 16 ++--- .../B05_Profile_UI_Style_Structures.css | 13 +++- B06_Section/B06_Section_Api_Types.ts | 2 + B06_Section/B06_Section_Engine_Culvert.py | 34 +++++----- .../B06_Section_UI_Cross_Culvert_Geom.ts | 16 ++++- .../B06_Section_UI_Cross_Structure_Panel.ts | 3 +- common_util/common_util_culvert_sets.ts | 42 +++++------- .../tester/test_b05_new_structure_defaults.py | 6 +- resources/tester/test_b06_culvert_set.py | 40 +++++++----- .../tester/test_b06_culvert_sets_mirror.py | 2 +- resources/tester/test_b06_structure_span.py | 18 ++--- 15 files changed, 125 insertions(+), 170 deletions(-) diff --git a/A00_Common/design/design_structure_form.ts b/A00_Common/design/design_structure_form.ts index a2ce50026..8ced4037d 100644 --- a/A00_Common/design/design_structure_form.ts +++ b/A00_Common/design/design_structure_form.ts @@ -128,7 +128,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = { const drawn = choices.find((ch) => ch.value === now); // 그림이 쓰는 값은 빈 보기 자리에 꼬리를 달아 보임 — 고른 값과 구별(54-13) const name = drawn ? choiceLabel(c, item, drawn, names) : now; - const blank = !tag ? "— 안 정함 —" : name ? `${name} (${tag})` : tag; + const blank = !tag ? "선택 안 함" : name ? `${name} (${tag})` : tag; select.replaceChildren( new Option(blank, ""), ...choices.map((ch) => new Option(choiceLabel(c, item, ch, names), ch.value)), @@ -145,8 +145,8 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = { input.value = values[item.id] ?? ""; return input; } - const hint = hooks.hint?.(c, item, state) || (item.default ? `${item.default} (기본)` : ""); - const input = numberInput(String(item.step ?? 0.1), "0", hint); + // 기본값은 새 상자 칸에 실제 값으로만(54-39) — 빈 칸 「(기본)」 안내 없음 + const input = numberInput(String(item.step ?? 0.1), "0", hooks.hint?.(c, item, state) ?? ""); if (item.kind === "auto") { // 보여 주는 칸(54-1) — 손 입력 칸 아님 const shown = document.createElement("output"); diff --git a/B04_PreProcess/B04_PreProcess_Router_Basins.py b/B04_PreProcess/B04_PreProcess_Router_Basins.py index 0d7916ff9..7b42a9e0d 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Basins.py +++ b/B04_PreProcess/B04_PreProcess_Router_Basins.py @@ -97,6 +97,9 @@ def _apply_new_defaults(points: list[PipePoint]) -> None: for field in definition.options: if field.key not in options and field.default not in (None, ""): options[field.key] = field.default + # 집수정 모양도 실제 값으로(54-39 · 54-36 기본 L형 — 화면 짝 `DEFAULT_BASIN_SHAPE`) + if options.get("inlet_type") == "집수정": + options.setdefault("inlet_structure", "L") point.options = options or None diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 87f980383..31a987769 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -49,7 +49,6 @@ import { } from "./B05_Profile_UI_Drainage_Facility_Fields"; import { revetChoiceRow } from "./B05_Profile_UI_Drainage_Revet_Choice"; import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form"; -import { branchOf } from "../A00_Common/design/design_structure_spec"; import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe"; import { BOX_SPEC, @@ -60,7 +59,6 @@ import { import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford"; import { fordHeightHint, fordHeightM } from "../A00_Common/design/design_structure_ford_calc"; import { - DEFAULT_BASIN_SHAPE, pipeDrawn, pipeExtraCount, pipeLengthM, @@ -275,19 +273,7 @@ export function createFacilityOptionsForm( clear: (key) => linkSource?.clear(key), set: (key, item) => linkSource?.set(key, item), }; - // 그림이 쓰는 값(54-13) — 빈 고르기 칸 = 정의 기본값(엔진 · 횡단도가 그 값으로 그림) · 보여 주기만 - let defaults: Record = {}; - const byDefault = (key: string | undefined): { value: string; tag: string } | null => { - const value = key ? defaults[key] : null; - return value === null || value === undefined || value === "" - ? null - : { value: String(value), tag: "기본" }; - }; - /** 빈 날개벽 설치 = 정의 기본 설치(옛 「있음」 · 빈 값 = 날개벽 · 엔진 `_wing_spec`). */ - const wingDefault = (key: string): { value: string; tag: string } => { - const value = String(defaults[key] ?? ""); - return { value: ["없음", "흙막이"].includes(value) ? value : "날개벽", tag: "기본" }; - }; + // 기본값은 새 시설 칸에 실제 값으로만(54-39) — 빈 칸에 「(기본)」 꼬리 · 안내 없음 // 유입 「자동(규칙)」 = 기슭막이 + 유입 형식 auto(옛 값 · 정의 기본) — 그림은 지형 규칙이 집수정 · 흙막이를 // 정함. 사용자가 유입 구조를 고를 때까지 저장도 그대로(흙막이 고름의 형식 revet 을 안 적음) let inletAuto: { structure?: string | number } | null = null; @@ -331,12 +317,12 @@ export function createFacilityOptionsForm( () => pipeForm.refresh(), ); const pipeForm = createSpecForm(specLinks, { - // 관 보호공 [연동] · [경사](54-30) — 유입 · 유출 상자 각각 한 줄(같은 값) · 집수정이면 숨김 + // 관 보호공 [연동] · [경사](54-30) — 유입 · 유출 상자 각각 한 줄(같은 값) · 길이 있는 흙막이만(54-40 · 집수정 · 안 고름은 숨김) fields: (c, values) => c.id === "outlet" - ? [paintTiers(values), outletChoice.paint(false)] + ? [paintTiers(values), outletChoice.paint(values.structure !== "기슭막이")] : c.id === "inlet" - ? [inletChoice.paint(values.structure === "집수정")] + ? [inletChoice.paint(values.structure !== "기슭막이")] : c.id === "soil_add" // 추가 흙막이 상자도 같은 줄(54-36 · 상자마다 한 줄 · 같은 값) ? [ revetChoiceRow( @@ -345,23 +331,15 @@ export function createFacilityOptionsForm( ).paint(false), ] : [], - shown: (c, item, values, index) => { + shown: (c, item) => { if (c.id === "inlet" && item.id === "structure" && inletIsAuto()) { const drawn = drawnNow(); return drawn ? { value: drawn.basin ? "집수정" : "기슭막이", tag: "자동" } : { value: "", tag: "자동(지형 규칙)" }; } - if (values[item.id]) return null; - if (item.id !== "form") return byDefault(branchOf(item, values)?.key); - const basin = c.id === "inlet" && values.structure === "집수정"; - const drawn = drawnNow(); - const form = basin - ? (drawn?.basin ?? DEFAULT_BASIN_SHAPE) - : c.id === "soil_add" - ? drawn?.extras[index] - : drawn?.forms[c.id]; - return form ? { value: form, tag: "기본" } : null; + // 빈 고르기 칸 = 「선택 안 함」 그대로(54-39 · 기본값 꼬리 없음) + return null; }, auto: (_c, item) => { if (item.id !== "length") return ""; @@ -370,16 +348,6 @@ export function createFacilityOptionsForm( }, // 흙막이 개수 = 유출 「추가 기슭막이(단)」(54-1) count: (c) => (c.id === "soil_add" ? pipeExtraCount(chainage) : undefined), - hint: (c, item, state) => { - if (item.default) return ""; // 명세 기본값 안내는 폼이 그림(추가 흙막이 · 54-36) - // 빈 칸 = 정의 기본값 「값 (기본)」(54-36) — 구조가 비었으면 기본 구조의 칸 - const raw = state[c.id]?.[0] ?? {}; - const top = c.items.find((i) => i.id === "structure"); - const values = - raw.structure || !top ? raw : { ...raw, structure: byDefault(top.key)?.value ?? "" }; - const fallback = item.kind === "number" ? byDefault(branchOf(item, values)?.key) : null; - return fallback ? `${fallback.value} (기본)` : ""; - }, onChange: (c, item) => { if (c.id === "inlet" && item.id === "structure") inletAuto = null; if (c.id === "inlet" && (item.id === "structure" || item.id === "form")) @@ -410,11 +378,6 @@ export function createFacilityOptionsForm( const fordNeed = (): number | null => fordHeightM(fordFlow, Number(fordForm.state().ford?.[0]?.width ?? "")); const fordForm = createSpecForm(specLinks, { - shown: (c, item, values) => { - if (values[item.id]) return null; - if (c.id !== "ford") return wingDefault(c.id === "inlet" ? "wing_in" : "wing_out"); - return item.id === "diameter" ? byDefault("pipe_diameter_mm") : null; - }, hint: (c, item, state) => c.id === "ford" && item.id === "height" ? fordHeightHint(fordFlow, Number(state.ford?.[0]?.width ?? "")) @@ -480,18 +443,7 @@ export function createFacilityOptionsForm( }; // ── BOX암거 = 사용자 구조물 표 02 명세(PLAN 52-2) — 본체 · 유입 구조 · 유출 구조 ── - const boxForm = createSpecForm(specLinks, { - onChange: () => emit(), - shown: (c, item, values) => { - if (values[item.id]) return null; - if (c.id !== "body") return wingDefault(c.id === "inlet" ? "wing_in" : "wing_out"); - const size = bodyOf({ - body_width_m: Number(defaults.body_width_m ?? 2), - body_height_m: Number(defaults.body_height_m ?? 2), - }).size; - return item.id === "size" && size ? { value: size, tag: "기본" } : null; - }, - }); + const boxForm = createSpecForm(specLinks, { onChange: () => emit() }); // 날개벽 칸 — 세월교 · BOX 가 같은 조각을 쓰던 자리 · 조정 행은 명세 유입 · 유출 칸으로 옮겨 붙음 const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS); const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS); @@ -630,7 +582,6 @@ export function createFacilityOptionsForm( // 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7). const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name])); const names = definition?.spec_names ?? {}; - defaults = Object.fromEntries((definition?.options ?? []).map((o) => [o.key, o.default])); if (facility === "pipe") { // 정의 파일의 옛 이름 「흙막이 추가」 — 명세 이름 「흙막이」가 앞섬(54-1) const { soil_add: _old, ...pipeNames } = names; diff --git a/B05_Profile/B05_Profile_UI_Structures_Balloon.ts b/B05_Profile/B05_Profile_UI_Structures_Balloon.ts index a627c2640..8023aecb8 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Balloon.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Balloon.ts @@ -7,7 +7,7 @@ * · 컨테이너 = 좌측 패널 폼과 같은 상자 — 명세 종류(`STRUCTURE_SPECS`)는 명세 컨테이너(선택 컨테이너는 * 인스턴스마다) · 그 밖 종류는 정의 옵션 그룹(`StructureType.groups`) * · 첫 옵션 = 지금 보이는 첫 입력 칸(자동 칸 · 갈래가 없어 가린 칸 · 끈 칸은 건너뜀) - * · 빈 값 = 등록부 기본값 「(기본)」 · 값 `auto` = 「(자동)」(54-13 표시와 같음) · 그것도 없으면 「—」 + * · 값 `auto` = 「(자동)」 · 빈 값 = 「—」(54-39 · 기본값으로 안 바꿈 · 기본값은 새 구조물 칸에 실제 값) * ========================================================================== */ import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures"; @@ -27,7 +27,6 @@ import { type StructureSpec, } from "../A00_Common/design/design_structure_spec"; -const DEFAULT_MARK = "(기본)"; const AUTO_MARK = "(자동)"; const NONE = "—"; @@ -62,8 +61,6 @@ function specLines( names = pipeNames; } const state = formState(spec, structure.type_id, structure.options ?? {}); - // 빈 칸이 그림에서 쓰는 값 — 등록부 기본값을 같은 길로 읽은 필수 컨테이너 값 - const fallback = formState(spec, structure.type_id, registryDefaults(type)); const groups = Object.fromEntries((type?.groups ?? []).map((g) => [g.id, g.name])); return spec.containers.flatMap((c) => { const title = containerTitle(c, names, groups[c.link]); @@ -72,9 +69,8 @@ function specLines( (entry) => entry.kind !== "auto" && (!entry.by || branchOf(entry, values)), ); const head = c.required ? title : `${title} ${index + 1}`; - const base = c.required ? (fallback[c.id]?.[0]?.[item?.id ?? ""] ?? "") : ""; return item - ? `${head}: ${itemLabel(c, item, names)} ${specValue(c, item, values, names, base)}` + ? `${head}: ${itemLabel(c, item, names)} ${specValue(c, item, values, names)}` : `${head}: ${NONE}`; }); }); @@ -113,7 +109,6 @@ function specValue( item: SpecItem, values: Instance, names: Record, - fallback: string, ): string { const shown = (raw: string): string => { const choice = choicesOf(item, values).find((ch) => ch.value === raw); @@ -121,8 +116,7 @@ function specValue( }; const raw = (values[item.id] ?? "").trim(); if (raw === "auto") return AUTO_MARK; - if (raw) return shown(raw); - return fallback.trim() ? `${shown(fallback.trim())} ${DEFAULT_MARK}` : NONE; + return raw ? shown(raw) : NONE; } /** 정의 옵션 그룹 줄 — 그룹마다 첫 옵션(정의에 있고 켜진 칸). */ @@ -139,22 +133,7 @@ function groupLines( const unit = option.unit ?? ""; const raw = options[option.key]; const value = - raw === "auto" - ? AUTO_MARK - : raw !== undefined && raw !== "" - ? `${raw}${unit}` - : option.default !== null && option.default !== undefined && option.default !== "" - ? `${option.default}${unit} ${DEFAULT_MARK}` - : NONE; + raw === "auto" ? AUTO_MARK : raw !== undefined && raw !== "" ? `${raw}${unit}` : NONE; return `${group.name}: ${option.label} ${value}`; }); } - -/** 등록부 기본값(키 → 값) — 빈 칸이 그림에서 쓰는 값. */ -function registryDefaults(type: StructureType | undefined): Record { - return Object.fromEntries( - (type?.options ?? []) - .filter((option) => option.default !== null && option.default !== undefined) - .map((option) => [option.key, option.default as string | number]), - ); -} diff --git a/B05_Profile/B05_Profile_UI_Structures_Fields.ts b/B05_Profile/B05_Profile_UI_Structures_Fields.ts index 8f446c04f..5e4c93d17 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Fields.ts @@ -8,6 +8,7 @@ * ========================================================================== */ import { chainageToStation, stationToChainage } from "./B05_Profile_Util_Station"; +import { DEFAULT_BASIN_SHAPE } from "../B06_Section/B06_Section_UI_Cross_Culvert_Basin"; export function field(labelText: string, input: HTMLElement): HTMLLabelElement { const wrapper = document.createElement("label"); @@ -65,14 +66,8 @@ export function optionControl( // 툴팁 — 표가 아직 안 읽는 칸이면 그 사유가 먼저(넣어도 안 바뀜을 알아야 함), 아니면 비울 때의 뜻. const tip = option.not_in_table ?? option.empty_means ?? ""; if (option.input === "select") { - // 빈 칸이 쓰는 정의 기본값은 빈 보기에 꼬리를 달아 보임 — 값은 안 고름(54-13) - const fallback = option.default === null || option.default === "" ? null : option.default; - const blank = - fallback !== null && fallback !== undefined - ? `${label(String(fallback))} (기본)` - : option.required - ? "— 선택 —" - : "— 안 정함 —"; + // 빈 보기 = 값 없음(54-39) — 기본값은 새 구조물 칸에 실제 값으로만(「(기본)」 꼬리 없음) + const blank = option.required ? "— 선택 —" : "선택 안 함"; // 정의에서 뺀 선택지(48)를 고른 옛 값 — 그 값만 남겨 보임(값을 잃지 않음) const kept = value && !option.choices.includes(value) ? [value] : []; const element = select([ @@ -107,11 +102,14 @@ export function optionControl( export function optionDefaults( options: ReadonlyArray<{ key: string; default: string | number | null }>, ): Record { - return Object.fromEntries( + const values: Record = Object.fromEntries( options .filter((option) => option.default !== null && option.default !== "") .map((option) => [option.key, option.default as string | number]), ); + // 배관 집수정 모양도 실제 값으로(54-39 · 54-36 기본 · 자동 배치 짝 `_apply_new_defaults`) + if (values.inlet_type === "집수정") values.inlet_structure ??= DEFAULT_BASIN_SHAPE; + return values; } /** 측점번호 + 잔여거리 두 칸 묶음. */ diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index 5d1bd5c5f..e0cadecda 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -554,8 +554,19 @@ grid-template-columns: 1fr 1fr; gap: 4px; } -/* [연동] [경사] 한 줄(54-36) — 안 여백만 줄임 · 높이 그대로 */ +/* [연동] [경사] 한 줄(54-36) — 안 여백만 줄임 */ .b05-structure__half-pair > button { padding-inline: 4px; white-space: nowrap; } + +/* 상자 안 작은 단추 템플릿(54-41) — 높이 = 숫자 칸 [-][+](`--b05-control-h`) · 글 · 안 여백도 그 높이에 · + * 상자 이름표 [빼기] · 상자 밖 [추가] · [삭제] · [리셋]은 제외 */ +.b05-structure__group-body button:not(.b05-structure__step) { + box-sizing: border-box; + height: var(--b05-control-h); + min-height: 0; + padding-block: 0; + font-size: var(--text-caption); + line-height: 1; +} diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index 174801264..9ee4a8d6b 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -209,6 +209,8 @@ export interface CulvertSideSpec { basin_after_m?: number | null; /** 집수정 높이(m) — 좌측 폼 「높이」. 없으면 횡단도 자동(I형 노견 접점 · ㄴ·ㄷ형 내공 높이). */ basin_height_m?: number | null; + /** 집수정 모양(I · L · U) — 폼 「형태」(54-39) · 횡단 patch 형식이 없을 때 그림이 씀. 없으면 집수정 없음. */ + basin_form?: string | null; /** 기슭막이 전면 기울기(1:n) 종전값 — 그림은 `revetWallSpec().lean`(표준경사 표)을 씀. */ face_slope?: number; /** 독립 기슭막이 기울기 판정 칸(저장 원본) — 설치 측 · 성토/절토 · 전면 기울기(B08 과 같은 칸). */ diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index 5b7fb798e..571b89ddc 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -168,8 +168,11 @@ def _side_spec( 구조가 집수정이면 기슭막이·보호공을 만들지 않는다 — 집수정 단면은 후속 작업이라 화면은 라벨만 쓴다(2026-08-19 사용자 확정). """ - structure = options.get(f"{side}_type") or defaults.get(f"{side}_type") or "기슭막이" + # 고르기 칸 빈 값 = 그 부재 없음(54-39 · 기본값으로 안 바꿈) + structure = options.get(f"{side}_type") or "" spec: dict[str, Any] = {"role": side, "structure": str(structure)} + if not structure: + return spec if spec["structure"] == INLET_STRUCTURE_BASIN: # 집수정 종방향 길이(3D 예상형상용, 기본 2m — 2026-08-23 사용자). # 전/후는 기슭막이와 같은 체계로 따로 지정한다(기본 각 1m — 2026-08-24 사용자). @@ -179,37 +182,37 @@ def _side_spec( ) spec["basin_before_m"] = _number( options.get("inlet_basin_before_m"), - _number(defaults.get("inlet_basin_before_m"), None), + None, ) spec["basin_after_m"] = _number( options.get("inlet_basin_after_m"), - _number(defaults.get("inlet_basin_after_m"), None), + None, ) # 집수정 높이(좌측 폼 · 비면 정의 기본값) — 횡단도 집수정 벽 높이. - spec["basin_height_m"] = _number( - options.get("inlet_basin_height_m"), _number(defaults.get("inlet_basin_height_m"), None) - ) + spec["basin_height_m"] = _number(options.get("inlet_basin_height_m"), None) + # 집수정 모양(54-39) — 빈 값 = 안 고름 + spec["basin_form"] = str(options.get("inlet_structure") or "") or None return spec # 높이를 안 적었으면 **횡단도가 그리는 관경 기준 최소 높이**(2026-09-14 · 옛 기본 2.5 걷음). height = _number( options.get(f"{side}_revet_height_m"), - _number(defaults.get(f"{side}_revet_height_m"), pipe_wall_min_height_m(diameter_m)), + pipe_wall_min_height_m(diameter_m), ) length = _number( options.get(f"{side}_revet_length_m"), - _number(defaults.get(f"{side}_revet_length_m"), None), + None, ) - form = options.get(f"{side}_revet_form") or defaults.get(f"{side}_revet_form") + form = options.get(f"{side}_revet_form") # 기준측점 전/후 몶(3D 예상형상 종방향 배치 — 2026-08-23 사용자). # 저장분이 없으면 길이 절반씩. before = _number( options.get(f"{side}_revet_before_m"), - _number(defaults.get(f"{side}_revet_before_m"), None), + None, ) after = _number( options.get(f"{side}_revet_after_m"), - _number(defaults.get(f"{side}_revet_after_m"), None), + None, ) spec.update( { @@ -235,7 +238,7 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]: diameter_mm = _number( values.get("pipe_diameter_mm"), _number(defaults.get("pipe_diameter_mm"), 1000.0) ) - kind = values.get("pipe_kind") or defaults.get("pipe_kind") + kind = values.get("pipe_kind") diameter_m = round((diameter_mm or 1000.0) / 1000.0, 3) return { "type": "pipe", @@ -244,10 +247,7 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]: "min_cover_m": MIN_PIPE_COVER_M, # 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다(2026-09-09). 한쪽만 기초유로 # 하는 일이 드물어 폼을 둘로 늘리지 않았다. 실무가 갈라 쓰면 그때 나눌 것. - "foundation": ( - str(values.get("revet_foundation") or defaults.get("revet_foundation") or "").strip() - or None - ), + "foundation": (str(values.get("revet_foundation") or "").strip() or None), "inlet": _side_spec(values, defaults, "inlet", diameter_m), "outlet": _side_spec(values, defaults, "outlet", diameter_m), } @@ -504,6 +504,8 @@ def load_culvert_sets(project_root: Path) -> dict[float, dict[str, Any]]: sets: dict[float, dict[str, Any]] = {} for point in parse_pipe_points(document.get("points")): if point.facility == PIPE_FACILITY_PIPE: + if (point.options or {}).get("pipe_diameter_mm") in (None, ""): + continue # 관경 안 고름 = 관 없음(54-39) spec = _culvert_set(point.options) elif point.facility == PIPE_FACILITY_FORD_BRIDGE: spec = _ford_set(point.options) diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index 37d6455f2..9d1afaf3f 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -186,7 +186,13 @@ export function computeCulvertLayout( : inletFillSlopeLen <= BASIN_MAX_FILL_SLOPE_M + 1e-6 ? "short" : null; - const choice: InletStructureChoice = inletStructure ?? "auto"; + // 횡단 patch 형식이 없으면 폼이 고른 집수정 모양(54-39) + const choice: InletStructureChoice = + inletStructure && inletStructure !== "auto" + ? inletStructure + : ((culvert.inlet.basin_form as InletStructureChoice | null | undefined) ?? + inletStructure ?? + "auto"); // 독립 기슭막이(관 숨김)는 집수정을 세우지 않는다 — 설치 측이 상단(유입 역할)일 때만 // 집수정이 서서 좌·우가 갈리던 원인(2026-08-29 사용자: 양쪽 다 성토 로직). const { shape: basinShape, reason: choiceReason } = resolveBasinChoice( @@ -194,7 +200,10 @@ export function computeCulvertLayout( ruleReason, inletOptions.basinLUAllowed, ); - const basinReason = culvert.hidden_pipe ? null : choiceReason; + // 유입 구조 · 집수정 모양을 안 고름 = 집수정 없음(54-39 · 기본 모양으로 안 세움) + const inletKind = culvert.inlet.structure; + const basinUnset = !inletKind || (inletKind === "집수정" && !["I", "L", "U"].includes(choice)); + const basinReason = culvert.hidden_pipe || basinUnset ? null : choiceReason; // ㄴ·ㄷ형은 사이즈 유지한 채 통째로 노견 끝점 일치 — 바닥(=관 invert)이 따라 오른다. if (basinReason && basinShape !== "I") { inlet.elevation = inletInfo.edge.elevation_m - (basinHeightM ?? basinAdjust.innerHeightM); @@ -368,6 +377,9 @@ export function computeCulvertLayout( } return null; } + // 구조 · 형태를 안 고른 배관 벽 = 그리지 않음(54-39 · 조정창 형태가 있으면 그 값) + const pickedForm = (spec.role === "inlet" ? adjInlet : adjOutlet).m || spec.revet_form; + if (!culvert.hidden_pipe && (spec.structure !== "기슭막이" || !pickedForm)) return null; // 형상: 배면 수직 + 계류측 1:n 평행사변형 띠(n = 표준경사). 높이는 vertical(계산용)이 들고 온다. const height = vertical?.height ?? Math.min(revetTargetHeight(diameter), revetHeightLimit(spec.revet_form)); diff --git a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts index e0d10169c..bcc02b4cb 100644 --- a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts @@ -540,7 +540,8 @@ export function buildStructurePanel( basinReset.classList.toggle("is-hidden", !isBasin); // I형은 내공(높이·폭) 개념이 없다 — 내공 줄을 숨긴다. for (const entry of basinSizeRows) { - entry.row.classList.toggle("is-hidden", !isBasin || structure === "I"); + // 화면에서 숨김(54-40 · 값 · 그림 계산은 그대로) + entry.row.classList.add("is-hidden"); if (isBasin) entry.readout.textContent = `${deps.basinAdjustFor()[entry.field].toFixed(1)}m`; } diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index 45d384d55..27e8d5574 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -96,39 +96,27 @@ function sideSpec( side: string, diameterM: number, ): CulvertSetSpec { - const structure = text(options[`${side}_type`]) ?? text(defaults[`${side}_type`]) ?? "기슭막이"; + // 고르기 칸 빈 값 = 그 부재 없음(54-39 · 기본값으로 안 바꿈) + const structure = text(options[`${side}_type`]) ?? ""; const spec: CulvertSetSpec = { role: side, structure }; + if (!structure) return spec; if (structure === INLET_STRUCTURE_BASIN) { spec.basin_length_m = num( options.inlet_basin_length_m, num(defaults.inlet_basin_length_m, 2.0), ); - spec.basin_before_m = num( - options.inlet_basin_before_m, - num(defaults.inlet_basin_before_m, null), - ); - spec.basin_after_m = num(options.inlet_basin_after_m, num(defaults.inlet_basin_after_m, null)); - spec.basin_height_m = num( - options.inlet_basin_height_m, - num(defaults.inlet_basin_height_m, null), - ); + spec.basin_before_m = num(options.inlet_basin_before_m, null); + spec.basin_after_m = num(options.inlet_basin_after_m, null); + spec.basin_height_m = num(options.inlet_basin_height_m, null); + spec.basin_form = text(options.inlet_structure); // 집수정 모양(54-39) — 빈 값 = 안 고름 return spec; } - const height = num( - options[`${side}_revet_height_m`], - num(defaults[`${side}_revet_height_m`], pipeWallMinHeightM(diameterM)), - ); - const length = num( - options[`${side}_revet_length_m`], - num(defaults[`${side}_revet_length_m`], null), - ); - const form = text(options[`${side}_revet_form`]) ?? text(defaults[`${side}_revet_form`]); - const before = num( - options[`${side}_revet_before_m`], - num(defaults[`${side}_revet_before_m`], null), - ); - const after = num(options[`${side}_revet_after_m`], num(defaults[`${side}_revet_after_m`], null)); + const height = num(options[`${side}_revet_height_m`], pipeWallMinHeightM(diameterM)); + const length = num(options[`${side}_revet_length_m`], null); + const form = text(options[`${side}_revet_form`]); + const before = num(options[`${side}_revet_before_m`], null); + const after = num(options[`${side}_revet_after_m`], null); spec.revet_form = form; spec.revet_height_m = height; spec.revet_length_m = length; @@ -153,13 +141,13 @@ function culvertSet( const diameterM = pipeDiameterM(diameterMm); return { type: "pipe", - pipe_kind: text(options.pipe_kind) ?? text(defaults.pipe_kind), + pipe_kind: text(options.pipe_kind), diameter_m: diameterM, min_cover_m: MIN_PIPE_COVER_M, // 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다. 한 공사에서 한쪽만 기초유로 // 하는 일은 드물어 폼을 둘로 늘리지 않았다(2026-09-09). 실무가 갈라 쓰는 것이 // 확인되면 그때 나눌 것. - foundation: text(options.revet_foundation) ?? text(defaults.revet_foundation), + foundation: text(options.revet_foundation), inlet: sideSpec(options, defaults, "inlet", diameterM), outlet: sideSpec(options, defaults, "outlet", diameterM), }; @@ -406,6 +394,8 @@ export function buildCulvertSets( else if (facility === FACILITY_BOX) spec = boxSet(values, registry); else if (facility === FACILITY_FORD_PAVEMENT) spec = fordPavementSet(values, registry); else if (facility === FACILITY_REVET) spec = revetSet(values); + else if (text(values.pipe_diameter_mm) === null) + continue; // 관경 안 고름 = 관 없음(54-39) else spec = culvertSet(values, registry); sets.set(round(chainage, 2), spec); } diff --git a/resources/tester/test_b05_new_structure_defaults.py b/resources/tester/test_b05_new_structure_defaults.py index 7f39ad1e2..3ec8dd184 100644 --- a/resources/tester/test_b05_new_structure_defaults.py +++ b/resources/tester/test_b05_new_structure_defaults.py @@ -33,7 +33,8 @@ def _defaults(type_id: str) -> dict: def test_새_관만_기본값_빈_키에만(): fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 800}) basins._apply_new_defaults([fresh]) - expected = {**_defaults("pipe"), "pipe_diameter_mm": 800} # 추천(먼저 들어간 값)이 이김 + # 추천(먼저 들어간 값)이 이김 · 집수정 모양 L 도 실제 값(54-39) + expected = {**_defaults("pipe"), "pipe_diameter_mm": 800, "inlet_structure": "L"} assert fresh.options == expected assert fresh.options["pipe_kind"] == "파형강관" and fresh.options["inlet_type"] == "집수정" box = PipePoint(chainage_m=20.0, source="stream", facility="box_culvert") @@ -62,7 +63,8 @@ def test_원본에_있던_관은_빈칸_그대로(monkeypatch): _, points = basins._build("p", Ctx(), [old]) by = {p.chainage_m: p for p in points} assert by[100.0].options is None # 옛 빈칸 그대로 - assert by[300.0].options == _defaults("pipe") # 새로 보충된 관 = 기본값 + # 새로 보충된 관 = 기본값 + 집수정 모양 L(54-39) + assert by[300.0].options == {**_defaults("pipe"), "inlet_structure": "L"} def test_월류_폭_기본값은_등록부_엔진과_같음(): diff --git a/resources/tester/test_b06_culvert_set.py b/resources/tester/test_b06_culvert_set.py index 11bbcd505..6c69b5fe2 100644 --- a/resources/tester/test_b06_culvert_set.py +++ b/resources/tester/test_b06_culvert_set.py @@ -33,44 +33,48 @@ def test_cross_reference_constants_match_plan(): assert "min_slope" not in _culvert_set(None) -def test_empty_options_fall_back_to_registry_defaults(): - """옵션이 빈 저장분은 레지스트리 기본값(Ø1000·L10)으로 채운다. - - 높이는 2026-09-14 브레인 판정으로 **관경 기준 최소 높이**(Ø1000 → 1.5 + 근입 0.5 = 2.0) — - 횡단도가 그리는 그 높이. 옛 기본 2.5 는 횡단도가 안 쓰던 값이라 걷음. - """ +def test_empty_options_have_no_members(): + """빈 고르기 칸 = 그 부재 없음(54-39 · 정의 기본값으로 안 바꿈) — 구조 · 형태 · 관종 · 기초 빈 값.""" spec = _culvert_set(None) assert spec["type"] == "pipe" - assert spec["diameter_m"] == 1.0 assert spec["min_cover_m"] == MIN_PIPE_COVER_M - # 54-36 기본: 유입 = 집수정(높이 1.5) · 유출 = 흙막이 찰 · 높이 1.5 - assert spec["inlet"]["structure"] == "집수정" - assert spec["inlet"]["basin_height_m"] == 1.5 + assert spec["pipe_kind"] is None + assert spec["foundation"] is None + assert spec["inlet"] == {"role": "inlet", "structure": ""} + assert spec["outlet"] == {"role": "outlet", "structure": ""} + + +def test_saved_values_drive_members(): + """고른 값만 부재가 됨 — 빈 높이 = 관경 기준 최소 높이(계산 · Ø1000 → 2.0) · 빈 형태 = 형태 없음.""" + spec = _culvert_set({"inlet_type": "집수정", "inlet_structure": "L", "outlet_type": "기슭막이"}) + assert spec["inlet"]["basin_form"] == "L" + assert spec["inlet"]["basin_height_m"] is None part = spec["outlet"] assert part["structure"] == "기슭막이" - assert part["revet_height_m"] == 1.5 - assert part["revet_length_m"] == 10 + assert part["revet_form"] is None + assert part["revet_height_m"] == 2.0 assert part["face_slope"] == REVET_FACE_SLOPE - # 보호공 = 낙차고(1.5) × 2 = 3.0m, 두께 0.45m(사용자 확정). - assert part["apron_length_m"] == 3.0 + # 보호공 = 낙차고(2.0) × 2 = 4.0m, 두께 0.45m(사용자 확정). + assert part["apron_length_m"] == 4.0 assert part["apron_thickness_m"] == 0.45 - assert part["revet_form"] == "돌쌓기(찰)" def test_basin_inlet_skips_revet_and_apron(): """유입구 = 집수정이면 그쪽 기슭막이·보호공을 만들지 않는다(라벨만).""" - spec = _culvert_set({"inlet_type": "집수정"}) + 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"] == 3.0 # 정의 기본 높이 1.5 × 2(54-36) + assert spec["outlet"]["apron_length_m"] == 4.0 # 빈 높이 = 관경 기준 최소 2.0 × 2 def test_string_diameter_and_custom_height(): """관경 문자열 저장분("800")과 사용자 높이 변경이 그대로 반영된다.""" - spec = _culvert_set({"pipe_diameter_mm": "800", "outlet_revet_height_m": 1.5}) + spec = _culvert_set( + {"pipe_diameter_mm": "800", "outlet_type": "기슭막이", "outlet_revet_height_m": 1.5} + ) 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 diff --git a/resources/tester/test_b06_culvert_sets_mirror.py b/resources/tester/test_b06_culvert_sets_mirror.py index 9ddacc95d..de989165a 100644 --- a/resources/tester/test_b06_culvert_sets_mirror.py +++ b/resources/tester/test_b06_culvert_sets_mirror.py @@ -28,7 +28,7 @@ TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" # 다섯 시설 × 기본값 폴백/명시값 섞기. 물넘이포장은 폭 절반이 옆 측점까지 걸친다. POINTS = [ - {"chainage_m": 20.0, "facility": "pipe", "options": None}, + {"chainage_m": 20.0, "facility": "pipe", "options": {"pipe_diameter_mm": 1000}}, { "chainage_m": 40.0, "facility": "pipe", diff --git a/resources/tester/test_b06_structure_span.py b/resources/tester/test_b06_structure_span.py index 33b1bc814..f8ff1c304 100644 --- a/resources/tester/test_b06_structure_span.py +++ b/resources/tester/test_b06_structure_span.py @@ -34,12 +34,12 @@ def test_registry_defines_basin_before_after(): def test_basin_side_spec_carries_before_after(): - """유입이 집수정이면 세트 제원에 길이와 전/후가 함께 실린다(레지스트리 기본값 경유).""" + """유입이 집수정이면 세트 제원에 길이와 전/후가 함께 실린다 — 빈 전/후 = 값 없음(54-39).""" spec = _culvert_set({"inlet_type": "집수정"})["inlet"] assert spec["structure"] == "집수정" assert spec["basin_length_m"] == 2.0 - assert spec["basin_before_m"] == 1.0 - assert spec["basin_after_m"] == 1.0 + assert spec["basin_before_m"] is None + assert spec["basin_after_m"] is None # 집수정 쪽에는 기슭막이가 없다 — 벽 제원이 섞이면 링크 판정이 10m로 번진다. assert "revet_length_m" not in spec @@ -60,10 +60,10 @@ def test_basin_before_after_follow_saved_options(): def test_revet_span_defaults_unchanged(): - """기슭막이 기본은 그대로 길이 10m·전/후 5·5다(이번 변경이 건드리지 않는다).""" + """빈 길이 · 전 · 후 = 값 없음(54-39 · 정의 기본값으로 안 읽음) · 빈 유출 구조 = 벽 없음.""" culvert = _culvert_set({"inlet_type": "기슭막이"}) - for role in ("inlet", "outlet"): - spec = culvert[role] - assert spec["revet_length_m"] == 10.0 - assert spec["revet_before_m"] == 5.0 - assert spec["revet_after_m"] == 5.0 + spec = culvert["inlet"] + assert spec["revet_length_m"] is None + assert spec["revet_before_m"] is None + assert spec["revet_after_m"] is None + assert culvert["outlet"] == {"role": "outlet", "structure": ""} From 5656b20af64ce9a8a9b314c71a6a60975b375aca Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 14:37:29 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test(structure):=2054-39=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98=20=EC=84=A4=EB=AA=85=20=EC=A4=84=20=EA=B8=B8=EC=9D=B4?= =?UTF-8?q?=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- resources/tester/test_b06_culvert_set.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/tester/test_b06_culvert_set.py b/resources/tester/test_b06_culvert_set.py index 6c69b5fe2..1970ecbbe 100644 --- a/resources/tester/test_b06_culvert_set.py +++ b/resources/tester/test_b06_culvert_set.py @@ -34,7 +34,7 @@ def test_cross_reference_constants_match_plan(): def test_empty_options_have_no_members(): - """빈 고르기 칸 = 그 부재 없음(54-39 · 정의 기본값으로 안 바꿈) — 구조 · 형태 · 관종 · 기초 빈 값.""" + """빈 고르기 칸 = 그 부재 없음(54-39) — 구조 · 형태 · 관종 · 기초 빈 값.""" spec = _culvert_set(None) assert spec["type"] == "pipe" assert spec["min_cover_m"] == MIN_PIPE_COVER_M @@ -45,7 +45,7 @@ def test_empty_options_have_no_members(): def test_saved_values_drive_members(): - """고른 값만 부재가 됨 — 빈 높이 = 관경 기준 최소 높이(계산 · Ø1000 → 2.0) · 빈 형태 = 형태 없음.""" + """고른 값만 부재 — 빈 높이 = 관경 기준 최소(Ø1000 → 2.0) · 빈 형태 = 없음.""" spec = _culvert_set({"inlet_type": "집수정", "inlet_structure": "L", "outlet_type": "기슭막이"}) assert spec["inlet"]["basin_form"] == "L" assert spec["inlet"]["basin_height_m"] is None