diff --git a/B05_Profile/B05_Profile_UI_Structures_Fields.ts b/B05_Profile/B05_Profile_UI_Structures_Fields.ts index 4950c574..5e695ecf 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Fields.ts @@ -32,6 +32,64 @@ export function select(options: ReadonlyArray<[string, string]>): HTMLSelectElem return element; } +/** 등록부 옵션 한 칸의 모양 — `StructureOptionField` 에서 이 칸이 쓰는 몫만. */ +interface OptionShape { + input: "select" | "number" | "text"; + choices: string[]; + default: string | number | null; + required?: boolean; +} + +/** + * 등록부 옵션 입력 한 칸 — **저장된 값만 칸에 넣고, 등록부 기본값은 제안으로만** 보임 + * (숫자·글 칸은 회색 글씨, 고르기 칸은 빈 보기 이름). 2026-09-14 브레인 판정 「기본값을 몰래 + * 확정으로 바꾸지 않는다」 — 칸에 채워 두면 저장이 그 값을 적어 사용자가 한 일 없이 확정이 됨. + * 고르기 칸은 늘 빈 보기를 둠 — 첫 보기를 미리 고르지 않음. + */ +export function optionControl( + option: OptionShape, + stored: string | number | undefined, +): HTMLInputElement | HTMLSelectElement { + const value = stored === undefined || stored === null ? "" : String(stored); + const suggested = option.default === null || option.default === "" ? "" : String(option.default); + if (option.input === "select") { + const blank = suggested + ? `— 안 정함 (제안 ${suggested}) —` + : option.required + ? "— 선택 —" + : "— 안 정함 —"; + const element = select([["", blank], ...option.choices.map((c) => [c, c] as [string, string])]); + element.value = value; + return element; + } + const element = + 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 ? "필수 입력" : ""; + 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; +} + /** 측점번호 + 잔여거리 두 칸 묶음. */ export interface StationFields { wrap: HTMLElement; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 023e352b..21b86c87 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -24,7 +24,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, numberInput, select } from "./B05_Profile_UI_Structures_Fields"; +import { field, optionControl, suggestButton } from "./B05_Profile_UI_Structures_Fields"; import { buildStructuresForm } from "./B05_Profile_UI_Structures_Form"; import { bindStructuresEvents, @@ -316,32 +316,18 @@ export function createStructuresSection( return; } optionRow.hidden = false; + const suggestions: Array<{ input: HTMLInputElement | HTMLSelectElement; suggested: string }> = + []; visible.forEach((option) => { - const preset = values[option.key] ?? option.default ?? ""; - let input: HTMLInputElement | HTMLSelectElement; - if (option.input === "select") { - const choices = option.choices.map((choice) => [choice, choice] as [string, string]); - // ⭐ 기본값 없는 고르기 칸은 **「안 정함」이 실제 상태** — 첫 보기를 미리 고르지 않음 - // (2026-09-14 브레인 판정, 옛 2026-08-17 「빈 보기 두지 않음」을 걷음). 사용자가 고른 적 - // 없는 첫 값이 저장돼 확정으로 굳는 뿌리였음(2026-09-13 `fill_concrete_mpa` 180 박힘도 - // 같은 병). 저장에는 빈 값이 가고 표가 사유를 보임. - if ((option.default ?? "") === "") { - choices.unshift(["", option.required === true ? "— 선택 —" : "— 안 정함 —"]); - } - input = select(choices); - input.value = String(preset); - } else if (option.input === "number") { - input = numberInput("0.1", "0"); - input.value = String(preset ?? ""); - if (option.required) (input as HTMLInputElement).placeholder = "필수 입력"; - } else { - input = document.createElement("input"); - (input as HTMLInputElement).type = "text"; - input.value = String(preset ?? ""); - } + // ⭐ 저장된 값만 칸에 · 등록부 기본값은 제안(회색 글씨·빈 보기 이름)으로만 — 고르기 칸은 + // 첫 보기를 미리 안 고름(2026-09-14 브레인 판정 ①②, `optionControl` 주석). + const input = optionControl(option, values[option.key]); + suggestions.push({ input, suggested: String(option.default ?? "") }); const label = option.unit ? `${option.label} (${option.unit})` : option.label; - // `enabled:false` 는 칸을 남기고 잠그기만 한다 — 값은 기본값이 그대로 저장된다. + // `enabled:false` 는 칸을 남기고 잠그기만 한다 — 값은 기본값이 그대로 저장된다 + // (고를 수 없는 칸이라 제안이 아니라 프로그램 값). const locked = option.enabled === false; + if (locked && !input.value) input.value = String(option.default ?? ""); if (locked) { input.disabled = true; input.classList.add("is-locked"); @@ -386,6 +372,9 @@ export function createStructuresSection( input.addEventListener("change", () => refresh(true)); } }); + if (suggestions.some((entry) => entry.suggested)) { + optionRow.append(suggestButton(suggestions)); + } syncOptionLock(); syncRangeDisplay(); } @@ -673,7 +662,8 @@ export function createStructuresSection( endFields.write(isInterval ? chainageM + DEFAULT_INTERVAL_LENGTH_M : null, step); memoField.value = ""; syncPlacementFields(); - renderOptionFields(type.managed_by ? undefined : defaultOptions(type)); + // 기본값을 값으로 안 채움 — 칸은 비우고 제안으로만(브레인 판정 ②). + renderOptionFields(); // BOX암거는 레지스트리 기본값(본체 2.0×2.0·날개벽 있음 1m/2m/45°)을 실어 폼을 // 연다 — [추가]만 눌러도 하류가 제원을 받는다(2026-08-17 사용자 확정 유지). syncFacilityForm(typeId === "box_culvert" ? defaultOptions(type) : {}); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts index f9fd343e..7ef4bc87 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts @@ -231,6 +231,13 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis ctx.optionInputs.find((entry) => entry.key === "length_m")?.input.focus(); return; } + // 높이도 비면 안 놓음 — 칸이 비면 횡단도 벽이 조용히 안 서고 수량도 안 섬. 적거나 + // [제안값 넣기]를 누를 것(2026-09-14 브레인 판정 「기본값을 몰래 확정으로 안 바꿈」). + const height = ctx.optionInputs.find((entry) => entry.key === "height_m"); + if (height?.isEmpty()) { + height.input.focus(); + return; + } const before = ctx.readBeforeM(); if (anchor - before < -0.005) { const beforeInput = ctx.optionInputs.find((entry) => entry.key === "before_m")?.input; diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Masonry.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Masonry.py index 8a981f6d..725b961a 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Masonry.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Masonry.py @@ -23,6 +23,8 @@ from B08_Quantity.B08_Quantity_Engine_UnitQuantity_StoneSpec import ( back_length_default_note, face_slope_ratio, fill_concrete_mpa, + STONE_SUPPLY_DEFAULT_NOTE, + STONE_SUPPLY_KEYS, is_collected_stone, load_stone_kind_table, stone_coefficients, @@ -487,6 +489,9 @@ def stone_masonry( # 채집석 — **캐서 쓰는 구조물**의 돌 체적. 사토에서 뺄 밑수이고 **여기서 빼지 않는다.** # ⚠ 밑수가 확정 5차로 바뀌었다 — 면석 몸통은 이제 **석적**(정면적×뒷길이×0.77)이고 # 막자갈은 **뒷채움 사다리꼴**이다. 옛 「입적 − 몸통 − 고임돌」 몫이 아니다. + if all(options.get(key) in (None, "") for key in STONE_SUPPLY_KEYS): + # 폼이 기본값을 값으로 안 보내게 된 뒤(2026-09-14) 빈 조달이 조용히 「채집」이 되지 않게. + notes.append(STONE_SUPPLY_DEFAULT_NOTE) if is_collected_stone(options): wedge = masonry_area * _num(table["wedge_stone_m3_per_m2"]) # None 이면 0 collected = max(stone_pile + wedge + rubble, 0.0) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_StoneSpec.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_StoneSpec.py index 05ecf4db..8e19a929 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_StoneSpec.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_StoneSpec.py @@ -279,6 +279,8 @@ STONE_MASONRY = { #: ⚠ 구조물마다 바꿀 수 있다 — 저장 제원에 「구입」이라 적힌 구조물만 공제에서 빠진다. STONE_SUPPLY_KEYS = ("stone_supply", "stone_source") STONE_SUPPLY_PURCHASED = {"구입", "구입품", "사서", "purchase", "purchased", "buy"} +#: 조달을 안 정한 구조물의 사유 — 셈은 확정 ② 「기본은 캔다」대로 채집이되 **안 정한 사실**을 보임. +STONE_SUPPLY_DEFAULT_NOTE = "돌 조달(채집·구입)을 안 정해 「채집」으로 셈 — 사용자 확정 ② 기본 · 구조물 상세에서 고르면 바뀜" #: ⚠⚠ **채집석 공제는 사토에서 한 번만 뺀다** (2026-09-09 세 창 확정 · 랩탑 메인의 통로에도 #: 같은 문장이 박혀 있다 — 두 곳이 같은 말이라야 나중에 누가 봐도 안 갈린다).