From 79ca220823735decefbc01d60885c809ef84a0f7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 13:49:14 +0900 Subject: [PATCH] auto: 2026-10-05 13:49 (EOMSANGDON-HOME) --- .../B04_PreProcess_Router_Basins.py | 28 ++++++ B05_Profile/B05_Profile_Structure_Types.json | 14 +-- .../B05_Profile_UI_Drainage_Facility.ts | 69 ++++---------- ...B05_Profile_UI_Drainage_Facility_Fields.ts | 92 +++++-------------- .../B05_Profile_UI_Drainage_Facility_Ford.ts | 24 ++--- .../B05_Profile_UI_Structures_Fields.ts | 41 +++------ .../B05_Profile_UI_Structures_Panel.ts | 21 +++-- ...B05_Profile_UI_Structures_Panel_Options.ts | 10 +- .../B05_Profile_UI_Style_Structures.css | 5 - .../tester/test_b05_new_structure_defaults.py | 86 +++++++++++++++++ .../tester/test_b05_revetment_sabang_basis.py | 3 +- 11 files changed, 203 insertions(+), 190 deletions(-) create mode 100644 resources/tester/test_b05_new_structure_defaults.py diff --git a/B04_PreProcess/B04_PreProcess_Router_Basins.py b/B04_PreProcess/B04_PreProcess_Router_Basins.py index 886996e70..0d7916ff9 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Basins.py +++ b/B04_PreProcess/B04_PreProcess_Router_Basins.py @@ -23,6 +23,7 @@ from fastapi import APIRouter from fastapi.responses import JSONResponse from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import drainage_dir +from B05_Profile.B05_Profile_Structures_Schema import structure_type_map from common_util.common_util_drainage_context import DrainageContext, load_drainage_context from common_util.common_util_drainage_detail import DrainageDetail, build_detail from common_util.common_util_drainage_pipes import ( @@ -30,6 +31,7 @@ from common_util.common_util_drainage_pipes import ( PIPE_SOURCE_USER, PipePoint, carry_facility_attributes, + carry_indices, clear_pipe_points, load_pipe_points, parse_pipe_points, @@ -64,14 +66,40 @@ def _build( if points: _retag(detail.pipes, points) rebuilt = [PipePoint(chainage_m=pipe.chainage_m, source=pipe.reason) for pipe in detail.pipes] + # 원본에 없던 자리 = 이번에 새로 놓인 관(자동 배치 · 간격 보충) — 등록부 기본값을 받을 관 + fresh = [ + pick is None + for pick in carry_indices( + [point.chainage_m for point in rebuilt], [point.chainage_m for point in points or []] + ) + ] # 계산기는 chainage만 다뤄 시설 종류·구간·옵션이 사라진다 — 원본에서 되붙인다 # (2026-08-17 계곡 통과 시설: 배관/BOX암거/물넘이/세월교). carried = carry_facility_attributes(rebuilt, points or []) # 되붙인 뒤에 추천을 얹는다 — 순서가 뒤바뀌면 승계된 사용자 값이 추천에 덮인다. _apply_recommendations(detail, carried) + _apply_new_defaults([point for point, new in zip(carried, fresh) if new]) return detail, carried +def _apply_new_defaults(points: list[PipePoint]) -> None: + """새로 놓인 관에만 등록부 기본값을 **빈 키에** 채움(PLAN 46-7 · 추천 뒤 — 추천 관경이 이김). + + 시설이 생길 때 기본값을 칸에 실제 값으로 넣어 두고 사용자가 고침 — 이미 있던 관의 빈 칸은 그대로 + (옛 프로젝트 빈칸 = 미확정 유지 · 사용자 결정 2026-10-05). + """ + types = structure_type_map() + for point in points: + definition = types.get(point.facility) + if definition is None: + continue + options = dict(point.options or {}) + for field in definition.options: + if field.key not in options and field.default not in (None, ""): + options[field.key] = field.default + point.options = options or None + + def _retag(pipes: list[StructureCandidate], points: list[PipePoint]) -> None: """확정 좌표로 되돌아온 관에 원래의 생성 사유를 다시 붙인다.""" by_chainage = {round(point.chainage_m, 2): point.source for point in points} diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 5353df625..43d980283 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -382,9 +382,10 @@ "label": "월류 폭", "input": "number", "unit": "m", - "default": null, - "required": true, - "phase": "detail" + "default": 5, + "required": false, + "phase": "detail", + "default_basis": "물넘이 월류 폭 기본 5m — 사용자 확정값(엔진 FORD_PAVEMENT_DEFAULT_WIDTH_M 과 같음)" }, { "key": "ford_slope_pct", @@ -458,9 +459,10 @@ "label": "월류 폭", "input": "number", "unit": "m", - "default": null, - "required": true, - "phase": "detail" + "default": 10, + "required": false, + "phase": "detail", + "default_basis": "세월교 월류 폭 기본 10m — 사용자 확정값(엔진 FORD_DEFAULT_WIDTH_M 과 같음)" }, { "key": "wing_in", diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 7cd62ad23..ee870d18b 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -53,7 +53,6 @@ import { optionalSelect, OUTLET_REVET_KEYS, putNumber, - REVET_COMMON_DEFAULTS, stepper, WING_IN_KEYS, WING_OUT_KEYS, @@ -246,11 +245,11 @@ export function createFacilityOptionsForm( root.hidden = true; // ── 배관 본체 — 관종 > 관경 한 행(2026-08-17 사용자 지시 4) ────────────── - // 관종 파형강관·관경 1000(사용자 확정 기본)은 **제안으로만** — 「안 정함」이 실제 상태(판정 ③). - const pipeMaterial = optionalSelect("— 안 정함 (제안 파형강관) —", ["흄관", "VR관", "파형강관"]); + // 기본값(관종 파형강관 · 관경 1000 · 등록부)은 시설이 생길 때 칸에 실제 값으로 들어옴(PLAN 46-7). + const pipeMaterial = optionalSelect("— 안 정함 —", ["흄관", "VR관", "파형강관"]); const pipeDiameter = document.createElement("select"); pipeDiameter.replaceChildren( - new Option("— 안 정함 (제안 Ø1000) —", ""), + new Option("— 안 정함 —", ""), ...["800", "1000", "1200", "1500"].map((size) => new Option(`Ø${size}`, size)), ); // 관보호공 날개벽 — **개소당 원단위**가 형식마다 붙박이라 고르는 칸이 있어야 물량이 선다 @@ -264,10 +263,10 @@ export function createFacilityOptionsForm( // ── 유입구 — 집수정 또는 기슭막이 택일(사용자 지시 5~9) ───────────────── const inletGroup = group("유입구"); - // 「안 정함」이면 기슭막이 칸을 보이되(제안) 값은 안 실음 — 기본값을 몰래 확정으로 안 바꿈(판정 ③). + // 「안 정함」이면 기슭막이 칸을 보이되 값은 안 실음. const inletType = document.createElement("select"); inletType.replaceChildren( - new Option("— 안 정함 (제안 기슭막이) —", ""), + new Option("— 안 정함 —", ""), ...INLET_KINDS.map((kind) => new Option(kind.label, kind.label)), ); const basinForm = optionalSelect("선택", [ @@ -285,7 +284,7 @@ export function createFacilityOptionsForm( ]); const basinMaterialField = labeled("형태", basinMaterial, "inlet_basin_material"); // 집수정 종방향 길이 — 3D 예상형상용, 전후 동일 배분·기본 2m(2026-08-23 사용자). - const basinLength = numberInput("0.1", "0", "제안 2"); + const basinLength = numberInput("0.1", "0"); // 기준측점 전 · 후 — 옛 횡단 조정창에만 있던 칸을 폼으로(계획서 24-4a). const basinSplit = createBasinSplitFields(basinLength); basinSplit.onChange(() => emit()); @@ -298,11 +297,7 @@ export function createFacilityOptionsForm( ]; const height = (): string => String(pipeWallMinHeightM(Number(pipeDiameter.value || "1000") / 1000)); - const inletRevet = createRevetmentFields(INLET_REVET_KEYS, { - ...REVET_COMMON_DEFAULTS, - form: "돌쌓기(찰)", - height, - }); + const inletRevet = createRevetmentFields(INLET_REVET_KEYS, { height }); // 첫 행 = [구조][형태] — 구조에 따라 집수정 형태와 기슭막이 형태가 갈아 끼워진다 // (2026-08-17 사용자 지시 3). // 조정창에서 옮겨 오는 행(단 수·옵션)이 붙을 자리(2026-08-29 사용자 지시 4). @@ -323,12 +318,8 @@ export function createFacilityOptionsForm( // ── 유출구 — 구조 선택지는 기슭막이뿐이지만 양식을 맞춘다(사용자 지시 2·4) ── const outletGroup = group("유출구"); - const outletType = optionalSelect("— 안 정함 (제안 기슭막이) —", ["기슭막이"]); - const outletRevet = createRevetmentFields(OUTLET_REVET_KEYS, { - ...REVET_COMMON_DEFAULTS, - form: "돌쌓기(메)", - height, - }); + const outletType = optionalSelect("— 안 정함 —", ["기슭막이"]); + const outletRevet = createRevetmentFields(OUTLET_REVET_KEYS, { height }); outletGroup.body.append( grid(labeled("구조", outletType, "outlet_type"), outletRevet.formField), ...outletRevet.rows, @@ -349,10 +340,10 @@ export function createFacilityOptionsForm( // 나눈다(2026-08-30 사용자). 설치 측이 한쪽이면 그 칸만 뜬다. // 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만 // 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다. - const revetSide = optionalSelect("— 안 정함 (제안 양쪽) —", ["양쪽", "좌", "우"]); + const revetSide = optionalSelect("— 안 정함 —", ["양쪽", "좌", "우"]); const revetTopRow = grid(labeled("설치 측", revetSide, "side")); - const revetInlet = createRevetSideGroup(INLET_REVET_KEYS, "돌쌓기(메)"); - const revetOutlet = createRevetSideGroup(OUTLET_REVET_KEYS, "돌쌓기(메)"); + const revetInlet = createRevetSideGroup(INLET_REVET_KEYS); + const revetOutlet = createRevetSideGroup(OUTLET_REVET_KEYS); /** 좌·우 이름표 — B06이 그 측점의 벽 방향을 알려 주면 바뀐다. 기본은 유입=좌. */ let revetSideLabels: { inlet: "좌" | "우"; outlet: "좌" | "우" } = { inlet: "좌", @@ -387,12 +378,12 @@ export function createFacilityOptionsForm( // ── BOX암거 — 본체 규격(프리셋 + 사용자 지정)·날개벽(유입·유출 개별) ──── // 폭·높이 자유 입력은 "사용자 지정"을 골랐을 때만 펼친다(2026-08-17 사용자 확정). - const boxSize = optionalSelect(`— 안 정함 (제안 ${BOX_SIZE_PRESETS[0][0]}) —`, [ + const boxSize = optionalSelect("— 안 정함 —", [ ...BOX_SIZE_PRESETS.map(([label]) => label), BOX_SIZE_CUSTOM, ]); - const boxWidth = numberInput("0.1", "0", `제안 ${BOX_SIZE_PRESETS[0][1]}`); - const boxHeight = numberInput("0.1", "0", `제안 ${BOX_SIZE_PRESETS[0][2]}`); + const boxWidth = numberInput("0.1", "0"); + const boxHeight = numberInput("0.1", "0"); const boxCustomRow = grid( labeled("본체 폭 (m)", boxWidth, "body_width_m"), labeled("본체 높이 (m)", boxHeight, "body_height_m"), @@ -415,33 +406,14 @@ export function createFacilityOptionsForm( boxWidth.value = String(preset[1]); boxHeight.value = String(preset[2]); } - // 「안 정함」도 폭·높이 칸을 접음 — 값이 없는 상태 그대로(제안은 빈 보기 이름). + // 「안 정함」도 폭·높이 칸을 접음 — 값이 없는 상태 그대로. boxCustomRow.hidden = boxSize.value !== BOX_SIZE_CUSTOM; } - // ── 물넘이·세월교 — 월류 폭·높이·경사·두께·길이·련 수(`_Ford` · 기본값은 제안으로만). + // ── 물넘이·세월교 — 월류 폭·높이·경사·두께·길이·련 수(`_Ford`). + // 기본값(등록부)은 시설이 생길 때 칸에 실제 값으로 들어옴 — 제안값 단추 없음(PLAN 46-7). const ford = createFordFields(() => emit()); - // [제안값 넣기] — 누른 때만 빈 칸에 제안값(관종·관경·구조·집수정 길이·설치 측·BOX 규격·날개벽·월류). - // 벽 칸(기슭막이)은 벽마다 단추가 따로 있음. 기본값을 몰래 확정으로 바꾸지 않는다(2026-09-14 판정 ③). - const suggest = document.createElement("button"); - suggest.type = "button"; - suggest.className = "b05-structure__suggest"; - suggest.textContent = "제안값 넣기"; - suggest.addEventListener("click", () => { - if (!pipeMaterial.value) pipeMaterial.value = "파형강관"; - if (!pipeDiameter.value) pipeDiameter.value = "1000"; - if (!inletType.value) inletType.value = INLET_KINDS[0].label; - if (!outletType.value) outletType.value = "기슭막이"; - if (!basinLength.value) basinLength.value = "2"; - if (!revetSide.value) revetSide.value = "양쪽"; - if (current === "box_culvert" && !boxSize.value) boxSize.value = BOX_SIZE_PRESETS[0][0]; - wingInFields.fillSuggested(); - wingOutFields.fillSuggested(); - if (current === "ford_pavement" || current === "ford_bridge") ford.fillSuggested(current); - emit(); - }); - // 10-A ⑲ 저장 규칙을 폼 머리에 드러냄 — 병합은 `_Drainage_Facility_Merge`(2026-09-14 브레인 판정). const saveNote = document.createElement("p"); saveNote.className = "b05-structure__owner-note"; @@ -451,7 +423,6 @@ export function createFacilityOptionsForm( // 세월교·물넘이 항목은 **관종·관경 바로 다음**에 둔다(2026-08-30 사용자 지시 2) — // 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다. root.append( - grid(suggest), saveNote, pipeRow, wingRow, @@ -593,7 +564,7 @@ export function createFacilityOptionsForm( } const text = (key: string): string => options[key] !== undefined ? String(options[key]) : ""; - // ⚠ 저장된 값만 칸에 — 없으면 「안 정함」 · 제안은 빈 보기 이름·회색 글씨(2026-09-14 판정 ③). + // 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7). pipeMaterial.value = text("pipe_kind"); pipeDiameter.value = text("pipe_diameter_mm"); // 병합 목록: 저장된 정본 둘(inlet_type·inlet_structure)로 라벨을 되찾는다. @@ -616,7 +587,7 @@ export function createFacilityOptionsForm( inletRevet.write(options); outletRevet.write(options); // 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다. - // 값이 없으면 「안 정함」(첫 프리셋 2.0×2.0 은 제안으로만). + // 값이 없으면 「안 정함」. const savedWidth = Number.parseFloat(text("body_width_m")); const savedHeight = Number.parseFloat(text("body_height_m")); const hasSaved = diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index f6617b4dc..69906964b 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -212,27 +212,13 @@ export interface RevetmentKeys { after?: string; } -/** 부위별 **제안값** — 형태는 유입 돌쌓기(찰)·유출 돌쌓기(메), 길이 10m. 관 벽 높이는 관경 기준 - * (함수로 받아 볼 때마다 셈). - * ⚠ 제안값은 **칸에 채우지 않고 회색 글씨로만** 보임 — 사용자가 적거나 [제안값 넣기]를 누른 때만 - * 값이 됨(2026-09-14 브레인 판정 「기본값을 몰래 확정으로 바꾸지 않는다」). 칸에 채워 두면 저장이 - * 그 값을 적어 **미확정이 저장 한 번에 확정으로 바뀜** — 사용자가 한 일이 없는데 금액이 섬. */ +/** 높이 칸 안내 — 관 벽은 비우면 관경 기준 최소 높이(엔진 `pipe_wall_min_height_m` · 함수로 받아 + * 볼 때마다 셈) · 빈 글이면 「설계자 입력」. 형태 · 길이 · 전후 기본값은 등록부가 시설이 생길 때 + * 칸에 실제 값으로 넣음(PLAN 46-7 · 제안값 단추 없음). */ export interface RevetmentDefaults { - form: string; - length: string; height: string | (() => string); - before: string; - after: string; } -/** 부위와 무관한 공통 제안 치수 — 길이 10m(전 5·후 5)·높이 2.5m(독립 기슭막이 · 관 벽은 관경 기준). */ -export const REVET_COMMON_DEFAULTS = { - length: "10", - height: "2.5", - before: "5", - after: "5", -} as const; - export const INLET_REVET_KEYS: RevetmentKeys = { form: "inlet_revet_form", length: "inlet_revet_length_m", @@ -273,16 +259,12 @@ function sabangBasisLines(): string[] { ]; } -export function createRevetSideGroup(keys: RevetmentKeys, formDefault: string): RevetSideGroup { +export function createRevetSideGroup(keys: RevetmentKeys): RevetSideGroup { const box = group("기슭막이"); const kind = document.createElement("select"); kind.replaceChildren(new Option("기슭막이", "기슭막이")); - // 높이는 제안 없음 — 설계자 입력(계획홍수위 + 여유고 · 2026-09-14 브레인 판정). 옛 2.5 는 근거 없어 걷음. - const fields = createRevetmentFields(keys, { - ...REVET_COMMON_DEFAULTS, - form: formDefault, - height: "", - }); + // 높이는 기본값 없음 — 설계자 입력(계획홍수위 + 여유고 · 2026-09-14 브레인 판정). + const fields = createRevetmentFields(keys, { height: "" }); const basis = document.createElement("p"); basis.className = "b05-route__note"; basis.replaceChildren( @@ -326,24 +308,24 @@ export function createRevetmentFields( keys: RevetmentKeys, defaults: RevetmentDefaults, ): RevetmentFields { - // ⚠ 제안값은 **회색 글씨(placeholder)로만** — 칸은 비워 둠(`RevetmentDefaults` 주석). - const suggestHeight = (): string => + const wallFloor = (): string => typeof defaults.height === "function" ? defaults.height() : defaults.height; const form = document.createElement("select"); form.replaceChildren( - new Option(`제안 ${defaults.form}`, ""), + new Option("— 안 정함 —", ""), ...REVET_FORMS.map((value) => new Option(value, value)), ); - const length = keys.length ? numberInput("0.1", "0", `제안 ${defaults.length}`) : null; - // 제안이 없는 칸(독립 기슭막이 높이)은 「설계자 입력」 — 값을 지어내지 않음. - const heightHint = (): string => (suggestHeight() ? `제안 ${suggestHeight()}` : "설계자 입력"); + const length = keys.length ? numberInput("0.1", "0") : null; + // 관 벽은 비우면 관경 기준 최소 높이로 섬(엔진) · 독립 기슭막이 높이는 「설계자 입력」. + const heightHint = (): string => + wallFloor() ? `비우면 관경 기준 ${wallFloor()}` : "설계자 입력"; const height = numberInput("0.1", "0", heightHint()); - // 관 벽 높이 제안은 관경을 따라감 — 칸을 볼 때마다 다시 셈. + // 관 벽 기준 높이는 관경을 따라감 — 칸을 볼 때마다 다시 셈. height.addEventListener("focus", () => (height.placeholder = heightHint())); // 기준측점 전/후(2026-08-23 사용자) — 구조물 패널(D군)과 같은 연동: // 전 + 후 = 길이, 나중에 고친 쪽이 살아남고 반대쪽이 재배분된다. - const before = keys.before && length ? numberInput("0.1", "0", `제안 ${defaults.before}`) : null; - const after = keys.after && length ? numberInput("0.1", "0", `제안 ${defaults.after}`) : null; + const before = keys.before && length ? numberInput("0.1", "0") : null; + const after = keys.after && length ? numberInput("0.1", "0") : null; let lastSplitEdit: "before" | "after" = "before"; const readNumber = (input: HTMLInputElement | null): number => { const value = input ? Number.parseFloat(input.value) : Number.NaN; @@ -367,25 +349,10 @@ export function createRevetmentFields( const beforeField = before ? labeled("기준측점 전 (m)", stepper(before, 0.5), keys.before) : null; const afterField = after ? labeled("기준측점 후 (m)", stepper(after, 0.5), keys.after) : 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 { @@ -393,7 +360,7 @@ export function createRevetmentFields( rows, lengthInput: length, write(options) { - // 저장된 값만 칸에 — 없으면 비워 두고 제안값은 회색 글씨로(몰래 채우지 않음). + // 받은 값만 칸에 — 없으면 비워 둠(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7). const text = (key: string): string => options[key] !== undefined ? String(options[key]) : ""; form.value = text(keys.form); @@ -447,8 +414,8 @@ export function createBasinSplitFields(length: HTMLInputElement): { read: (target: Record) => void; onChange: (handler: () => void) => void; } { - const before = numberInput("0.1", "0", "제안 1"); - const after = numberInput("0.1", "0", "제안 1"); + const before = numberInput("0.1", "0"); + const after = numberInput("0.1", "0"); let last: HTMLInputElement = before; const sync = (): void => { const total = Number.parseFloat(length.value); @@ -528,10 +495,6 @@ export const WING_OUT_KEYS: WingKeys = { angle: "wing_out_angle_deg", }; -/** 날개벽 기본 제원 — 설치 있음·짧은쪽 높이 1m·길이 2m·각도 45° - * (2026-08-17 사용자 확정). 유입·유출이 같은 값을 쓴다. */ -const WING_DEFAULTS = { install: "있음", height: "1", length: "2", angle: "45" } as const; - interface WingFields { root: HTMLFieldSetElement; /** 횡단도에서 그 측 날개벽·측벽을 고를 때 칸을 세우는 자리(B06 전용, 2026-08-30). @@ -541,18 +504,16 @@ interface WingFields { syncVisibility: () => void; write: (options: Record) => void; read: (target: Record) => void; - /** [제안값 넣기] — 빈 칸에만 기본 제원을 넣음. */ - fillSuggested: () => void; } /** 날개벽 한 벌(설치 유무 + 짧은쪽 높이·길이·각도) — 유입·유출이 공유하는 조각. * 설치 "없음"이면 제원 칸을 접는다(기슭막이 보호공 치수와 같은 패턴). - * ⚠ 기본 제원(있음·1·2·45, 사용자 확정)은 **제안으로만** — 칸은 비워 둠(2026-09-14 판정 ③). */ + * 기본 제원(있음·1·2·45, 사용자 확정)은 등록부 기본값 — 시설이 생길 때 칸에 실제 값(PLAN 46-7). */ export function createWingFields(title: string, keys: WingKeys): WingFields { - const install = optionalSelect(`— 안 정함 (제안 ${WING_DEFAULTS.install}) —`, ["있음", "없음"]); - const height = numberInput("0.1", "0", `제안 ${WING_DEFAULTS.height}`); - const length = numberInput("0.1", "0", `제안 ${WING_DEFAULTS.length}`); - const angle = numberInput("1", "0", `제안 ${WING_DEFAULTS.angle}`); + const install = optionalSelect("— 안 정함 —", ["있음", "없음"]); + const height = numberInput("0.1", "0"); + const length = numberInput("0.1", "0"); + const angle = numberInput("1", "0"); // 숫자 칸은 기슭막이·집수정과 같은 [-][값][+] 묶음으로 맞춘다(2026-08-30 사용자). const heightField = labeled("짧은쪽 높이 (m)", stepper(height, 0.1), keys.height); @@ -594,12 +555,5 @@ export function createWingFields(title: string, keys: WingKeys): WingFields { putNumber(target, keys.length, length.value); putNumber(target, keys.angle, angle.value); }, - fillSuggested() { - if (!install.value) install.value = WING_DEFAULTS.install; - if (!height.value) height.value = WING_DEFAULTS.height; - if (!length.value) length.value = WING_DEFAULTS.length; - if (!angle.value) angle.value = WING_DEFAULTS.angle; - syncVisibility(); - }, }; } diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts index 11e0edb36..c1a5a8ae2 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts @@ -3,10 +3,9 @@ * 물넘이포장·세월교 칸 묶음 — 월류 폭·높이·바닥 경사·포장 두께·길이·련 수 + 개략 단면 줄. * * 서브폼 본체(`_Drainage_Facility.ts`)가 700줄에 닿아 떼어 냄(2026-09-14). - * ⚠ 기본값을 칸에 안 채움(2026-09-14 브레인 판정 「기본값을 몰래 확정으로 바꾸지 않는다」): - * 월류 폭 세월교 10m·물넘이 5m(사용자 확정)는 **회색 제안**, 월류 높이는 설계유량·폭으로 되짚은 - * **필요 최소 수심을 회색 제안**으로만 — 사용자가 적거나 [제안값 넣기]를 누른 때만 값. - * 적은 높이가 필요 수심보다 낮으면 여전히 되돌림(계산값 미만은 안 받음). + * 월류 폭 기본값(세월교 10m · 물넘이 5m · 등록부)은 시설이 생길 때 칸에 실제 값으로 들어옴 + * (PLAN 46-7 · 제안값 단추 없음). 빈 칸 안내 = 엔진이 비운 칸을 읽는 값(폭 기본 · 필요 수심). + * 적은 높이가 필요 수심보다 낮으면 되돌림(계산값 미만은 안 받음). * ========================================================================== */ import { @@ -35,7 +34,6 @@ export interface FordFields { sync: () => void; write: (kind: FordKind, options: Record) => void; read: (kind: FordKind, target: Record) => void; - fillSuggested: (kind: FordKind) => void; } export function createFordFields(emit: () => void): FordFields { @@ -66,14 +64,15 @@ export function createFordFields(emit: () => void): FordFields { /** 현재 조건(설계유량·월류 폭)의 필요 최소 수심(m). 계산 불가면 null. */ let minDepthM: number | null = null; - const suggestedWidth = (kind: FordKind): number => + /** 폭을 비우면 엔진이 쓰는 폭(`B06_Section_Engine_Culvert.FORD_*_DEFAULT_WIDTH_M` 짝). */ + const emptyWidth = (kind: FordKind): number => kind === "ford_bridge" ? FORD_BRIDGE_DEFAULT_WIDTH_M : FORD_PAVEMENT_DEFAULT_WIDTH_M; function sync(): void { - const widthM = Number.parseFloat(width.value || width.dataset.suggested || ""); + const widthM = Number.parseFloat(width.value || width.dataset.empty || ""); const section = designFlowM3s !== null ? fordSection(designFlowM3s, widthM) : null; minDepthM = section ? Number(section.depthM.toFixed(2)) : null; - height.placeholder = minDepthM !== null ? `제안 필요 수심 ${minDepthM.toFixed(2)}` : ""; + height.placeholder = minDepthM !== null ? `필요 수심 ${minDepthM.toFixed(2)} 이상` : ""; if (designFlowM3s === null) { summary.textContent = "담당 유역의 설계유량이 아직 없습니다 — [유역 분석] 후 개략 단면이 나옵니다."; @@ -121,8 +120,8 @@ export function createFordFields(emit: () => void): FordFields { options[key] !== undefined ? String(options[key]) : ""; count.value = kind === "ford_bridge" ? text("pipe_count") : ""; width.value = text("ford_width_m"); - width.dataset.suggested = String(suggestedWidth(kind)); - width.placeholder = `제안 ${suggestedWidth(kind)}`; + width.dataset.empty = String(emptyWidth(kind)); + width.placeholder = `비우면 ${emptyWidth(kind)}`; height.value = text("ford_height_m"); slope.value = text("ford_slope_pct"); thickness.value = text("thickness_cm"); @@ -142,10 +141,5 @@ export function createFordFields(emit: () => void): FordFields { const pipes = Number.parseInt(count.value, 10); if (Number.isFinite(pipes) && pipes > 0) target.pipe_count = pipes; }, - fillSuggested(kind) { - if (!width.value) width.value = String(suggestedWidth(kind)); - sync(); - if (!height.value && minDepthM !== null) height.value = minDepthM.toFixed(2); - }, }; } diff --git a/B05_Profile/B05_Profile_UI_Structures_Fields.ts b/B05_Profile/B05_Profile_UI_Structures_Fields.ts index 904e13c5f..2581b79ee 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Fields.ts @@ -61,9 +61,8 @@ interface OptionShape { } /** - * 등록부 옵션 입력 한 칸 — **저장된 값만 칸에 넣고, 등록부 기본값은 제안으로만** 보임 - * (숫자·글 칸은 회색 글씨, 고르기 칸은 빈 보기 이름). 2026-09-14 브레인 판정 「기본값을 몰래 - * 확정으로 바꾸지 않는다」 — 칸에 채워 두면 저장이 그 값을 적어 사용자가 한 일 없이 확정이 됨. + * 등록부 옵션 입력 한 칸 — **받은 값만 칸에 넣음**. 새 구조물은 부르는 쪽이 등록부 기본값을 값으로 + * 실어 줌(PLAN 46-7 · 제안값 단추 없음) · 옛 기록의 빈 칸은 빈 칸 그대로(미확정). * 고르기 칸은 늘 빈 보기를 둠 — 첫 보기를 미리 고르지 않음. */ export function optionControl( @@ -71,15 +70,10 @@ export function optionControl( stored: string | number | undefined, ): HTMLInputElement | HTMLSelectElement { const value = stored === undefined || stored === null ? "" : String(stored); - const suggested = option.default === null || option.default === "" ? "" : String(option.default); // 툴팁 — 표가 아직 안 읽는 칸이면 그 사유가 먼저(넣어도 안 바뀜을 알아야 함), 아니면 비울 때의 뜻. const tip = option.not_in_table ?? option.empty_means ?? ""; if (option.input === "select") { - const blank = suggested - ? `— 안 정함 (제안 ${suggested}) —` - : option.required - ? "— 선택 —" - : "— 안 정함 —"; + const blank = option.required ? "— 선택 —" : "— 안 정함 —"; const element = select([["", blank], ...option.choices.map((c) => [c, c] as [string, string])]); element.value = value; if (tip) element.title = tip; @@ -89,7 +83,7 @@ export function optionControl( option.input === "number" ? numberInput("0.1", "0") : document.createElement("input"); if (option.input !== "number") element.type = "text"; element.value = value; - element.placeholder = suggested ? `제안 ${suggested}` : option.required ? "필수 입력" : ""; + element.placeholder = option.required ? "필수 입력" : ""; if (tip) element.title = tip; const limit = option.warn_above; if (typeof limit === "number") { @@ -105,24 +99,15 @@ export function optionControl( return element; } -/** [제안값 넣기] — **누른 때만** 빈 칸에 등록부 기본값을 넣음(적은 칸은 안 건드림) · 층따기 단추와 같은 모양. */ -export function suggestButton( - entries: ReadonlyArray<{ input: HTMLInputElement | HTMLSelectElement; suggested: string }>, -): HTMLButtonElement { - const button = document.createElement("button"); - button.type = "button"; - button.className = "b05-structure__suggest"; - button.textContent = "제안값 넣기"; - button.addEventListener("click", () => { - const filled = entries.filter((entry) => entry.suggested && !entry.input.value); - filled.forEach((entry) => { - entry.input.value = entry.suggested; - entry.input.dispatchEvent(new Event("input", { bubbles: true })); - }); - // 반영은 한 번만 — 칸마다 저장 흐름이 돌지 않게 마지막 칸만 change 를 울림. - filled.at(-1)?.input.dispatchEvent(new Event("change", { bubbles: true })); - }); - return button; +/** 새 구조물 칸에 실을 등록부 기본값(PLAN 46-7) — 기본값이 있는 칸만 · 없는 칸은 빈 칸(미확정). */ +export function optionDefaults( + options: ReadonlyArray<{ key: string; default: string | number | null }>, +): Record { + return Object.fromEntries( + options + .filter((option) => option.default !== null && option.default !== "") + .map((option) => [option.key, option.default as string | number]), + ); } /** 측점번호 + 잔여거리 두 칸 묶음. */ diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 63c0470d9..7989e539c 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -22,7 +22,7 @@ import { } from "./B05_Profile_Api_Structures"; import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { type FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility"; -import { field } from "./B05_Profile_UI_Structures_Fields"; +import { field, optionDefaults } from "./B05_Profile_UI_Structures_Fields"; import { renderOptionFields as renderOptionFieldsInto } from "./B05_Profile_UI_Structures_Panel_Options"; import { bindSummaryPickers } from "./B05_Profile_UI_Structures_Groups"; import { buildStructuresForm } from "./B05_Profile_UI_Structures_Form"; @@ -239,11 +239,11 @@ export function createStructuresSection( /** 종류에 맞춰 부속 옵션 서브폼을 켠다(계곡 통과 시설·독립 기슭막이, 값은 인자로). */ function syncFacilityForm( - options: Record = {}, + options?: Record, designFlow: number | null = null, ): void { const kind = facilityFormKind(currentType()); - facilityOptions.setFacility(kind, options, designFlow); + facilityOptions.setFacility(kind, options ?? newDefaults(), designFlow); // 「구조물 옵션 보이기」에서 끈 옵션은 시설 폼에서도 가림(값은 옛 값 그대로 저장) facilityOptions.setHiddenOptions( kind, @@ -318,11 +318,16 @@ export function createStructuresSection( } /** 타입의 옵션 스키마대로 입력 칸을 다시 그린다 — 그리는 규칙은 `_Structures_Panel_Options`. */ - function renderOptionFields(values: Record = {}): void { - renderOptionFieldsInto(optionContext, values); + function renderOptionFields(values?: Record): void { + renderOptionFieldsInto(optionContext, values ?? newDefaults()); regroup(); } + /** 값 없이 그리면 새 구조물 — 등록부 기본값을 칸에 실제 값으로(PLAN 46-7 · 옛 기록은 받은 값만). */ + function newDefaults(): Record { + return optionDefaults(currentType()?.options ?? []); + } + /** 고른 구조물군의 타입을 종류 목록에 채운다 — A군은 계곡 통과 시설(managed_by)도 * 포함한다(추가 시 관 지점 정본으로 간다, 2026-08-17 통합). 첫 항목은 빈칸 — * 초기·리셋 상태를 "안 고름"으로 드러낸다. */ @@ -639,11 +644,9 @@ export function createStructuresSection( endFields.write(isInterval ? chainageM + DEFAULT_INTERVAL_LENGTH_M : null, step); memoField.value = ""; syncPlacementFields(); - // 기본값을 값으로 안 채움 — 칸은 비우고 제안으로만(브레인 판정 ②). + // 새 구조물 — 등록부 기본값을 칸에 실제 값으로(PLAN 46-7 · 사용자가 고치면 됨). renderOptionFields(); - // BOX암거도 기본값을 안 실음 — 제안으로만(판정 ③ · 옛 「[추가]만 눌러도 제원」 걷음). - // 비운 칸은 하류(횡단도)가 등록부 기본값으로 그림 — 저장만 안 함. - syncFacilityForm({}); + syncFacilityForm(); // 임시 배치도 즉시 진행(A군). if (type.managed_by) commitTempPipe(); syncButtons(); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts index de86abde6..f557893d3 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts @@ -10,7 +10,7 @@ import { isB05Option, type StructureType } from "./B05_Profile_Api_Structures"; import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; -import { field, optionControl, suggestButton } from "./B05_Profile_UI_Structures_Fields"; +import { field, optionControl } from "./B05_Profile_UI_Structures_Fields"; import { isOptionVisible } from "./B05_Profile_UI_Structures_Summary"; /** 패널이 쥔 값 중 옵션 그리기에 필요한 것들. 이름은 분리 전 지역변수와 같다. */ @@ -56,12 +56,9 @@ export function renderOptionFields( return; } optionRow.hidden = false; - const suggestions: Array<{ input: HTMLInputElement | HTMLSelectElement; suggested: string }> = []; shown.forEach((option) => { - // ⭐ 저장된 값만 칸에 · 등록부 기본값은 제안(회색 글씨·빈 보기 이름)으로만 — 고르기 칸은 - // 첫 보기를 미리 안 고름(2026-09-14 브레인 판정 ①②, `optionControl` 주석). + // 받은 값만 칸에 — 새 구조물은 부르는 쪽(패널)이 등록부 기본값을 `values` 로 실어 줌(PLAN 46-7). const input = optionControl(option, values[option.key]); - suggestions.push({ input, suggested: String(option.default ?? "") }); const label = `${option.unit ? `${option.label} (${option.unit})` : option.label}${option.not_in_table ? " · 표에 안 쓰임" : ""}`; // `enabled:false` 는 칸을 남기고 잠그기만 한다 — 값은 기본값이 그대로 저장된다 // (고를 수 없는 칸이라 제안이 아니라 프로그램 값). @@ -113,9 +110,6 @@ export function renderOptionFields( input.addEventListener("change", () => refresh(true)); } }); - if (suggestions.some((entry) => entry.suggested)) { - optionRow.append(suggestButton(suggestions)); - } ctx.syncOptionLock(); ctx.syncRangeDisplay(); } diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index 43b877db1..57e713798 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -221,11 +221,6 @@ line-height: 1.3; } -/* 근거 줄로 옆 칸이 길어져도 [제안값 넣기]가 세로로 늘지 않게. */ -.b05-structure__suggest { - align-self: start; -} - /* 시작·기준·종료 측점 = 3행. 한 행은 [라벨][측점][+거리] 가로 배치 * (2026-08-17 사용자 지시 2). */ .b05-structure__position-row { diff --git a/resources/tester/test_b05_new_structure_defaults.py b/resources/tester/test_b05_new_structure_defaults.py new file mode 100644 index 000000000..4fcdd9ed2 --- /dev/null +++ b/resources/tester/test_b05_new_structure_defaults.py @@ -0,0 +1,86 @@ +"""새 구조물 기본값 — 「제안값 넣기」 걷음 · 생길 때 등록부 기본값을 칸에 실제 값으로 (PLAN 46-7). + +못박는 것 + - 자동 배치로 새로 놓인 관 = 등록부 기본값을 빈 키에 채움 · 추천 관경이 기본값을 이김. + - 이미 있던 관(원본에 있던 자리)의 빈 칸은 그대로(옛 프로젝트 빈칸 = 미확정 유지). + - 월류 폭 기본값(세월교 10 · 물넘이 5)은 등록부에 · 엔진 상수와 같음. + - 화면: 제안값 단추 · 「제안 …」 글이 없음 · 값 없이 그리면 등록부 기본값(`newDefaults`). +""" + +from __future__ import annotations + +from pathlib import Path + +from B04_PreProcess import B04_PreProcess_Router_Basins as basins +from B05_Profile.B05_Profile_Structures_Schema import structure_type_map +from B06_Section.B06_Section_Engine_Culvert import ( + FORD_DEFAULT_WIDTH_M, + FORD_PAVEMENT_DEFAULT_WIDTH_M, +) +from common_util.common_util_drainage_pipes import PipePoint + +ROOT = Path(__file__).resolve().parents[2] + + +def _defaults(type_id: str) -> dict: + return { + o.key: o.default + for o in structure_type_map()[type_id].options + if o.default not in (None, "") + } + + +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} # 추천(먼저 들어간 값)이 이김 + 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") + basins._apply_new_defaults([box]) + assert box.options["body_width_m"] == 2.0 and box.options["wing_in"] == "있음" + + +def test_원본에_있던_관은_빈칸_그대로(monkeypatch): + """`_build` — 원본 목록에 있던 자리는 기본값을 안 받고, 새 자리만 받음.""" + + class Pipe: + def __init__(self, chainage_m: float, reason: str) -> None: + self.chainage_m, self.reason = chainage_m, reason + + class Detail: + pipes = [Pipe(100.0, "stream"), Pipe(300.0, "spacing")] + basins: list = [] + + monkeypatch.setattr(basins, "build_detail", lambda *_: Detail()) + monkeypatch.setattr(basins, "drainage_dir", lambda _: "x") + + class Ctx: + vertices: list = [] + + old = PipePoint(chainage_m=100.0, source="stream") # 옛 관 — 옵션 없음 + _, 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") # 새로 보충된 관 = 기본값 + + +def test_월류_폭_기본값은_등록부_엔진과_같음(): + assert _defaults("ford_bridge")["ford_width_m"] == FORD_DEFAULT_WIDTH_M + assert _defaults("ford_pavement")["ford_width_m"] == FORD_PAVEMENT_DEFAULT_WIDTH_M + + +def test_화면에_제안값_단추_제안_글이_없음(): + files = [ + "B05_Profile/B05_Profile_UI_Drainage_Facility.ts", + "B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts", + "B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts", + "B05_Profile/B05_Profile_UI_Structures_Fields.ts", + "B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts", + ] + for name in files: + text = (ROOT / name).read_text(encoding="utf-8") + assert "제안값 넣기" not in text and "b05-structure__suggest" not in text, name + assert "`제안 " not in text and '"제안 ' not in text and "(제안 " not in text, name + panel = (ROOT / "B05_Profile/B05_Profile_UI_Structures_Panel.ts").read_text(encoding="utf-8") + assert "values ?? newDefaults()" in panel and "options ?? newDefaults()" in panel diff --git a/resources/tester/test_b05_revetment_sabang_basis.py b/resources/tester/test_b05_revetment_sabang_basis.py index c0071504a..e48c6ad13 100644 --- a/resources/tester/test_b05_revetment_sabang_basis.py +++ b/resources/tester/test_b05_revetment_sabang_basis.py @@ -42,5 +42,6 @@ def test_TS_가_쓰는_칸이_파일에_있다() -> None: def test_B05_높이_칸은_제안_없이_설계자_입력() -> None: - assert 'height: "",' in FIELDS and '"설계자 입력"' in FIELDS + # 독립 기슭막이 높이 = 기본값 없음 · 「설계자 입력」 안내(제안값 단추 걷음 · PLAN 46-7) + assert 'createRevetmentFields(keys, { height: "" })' in FIELDS and '"설계자 입력"' in FIELDS assert "기슭막이 높이 없음 — 근입만 그림" in CHROME