fix(b05,b08): 구조물 목록 폼이 등록부 기본값을 값으로 안 채움 — 제안으로만(폼 ②)

- 칸에는 저장된 값만 · 기본값은 회색 글씨(숫자)·빈 보기 이름(고르기)으로 제안 · [제안값 넣기] 누른 때만 값
- 새 구조물 추가도 기본값을 안 실음(defaultOptions 로 채우던 자리) · 잠긴 칸(enabled:false)만 프로그램 값 유지
- C군(범위 계산 타입)은 길이처럼 높이도 비면 안 놓음 — 비면 횡단도 벽이 조용히 안 서던 길
- B08: 돌 조달을 안 정하면 「채집으로 셈」 사유를 보임(조용히 채집 되지 않게)
- ORCA 936be972: 돌쌓기(메) 고르면 높이·길이·전·설치측 빔 + 제안 · 단추로 채워짐 확인(저장 안 함)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-14 09:21:27 +09:00
co-authored by Claude Opus 5
parent 7eecfacca3
commit 2744f1bcbb
5 changed files with 87 additions and 25 deletions
@@ -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;
+15 -25
View File
@@ -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) : {});
@@ -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;
@@ -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)
@@ -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 세 창 확정 · 랩탑 메인의 통로에도
#: 같은 문장이 박혀 있다 — 두 곳이 같은 말이라야 나중에 누가 봐도 안 갈린다).