feat(B06): 구조물 상세 제원을 B06 폼에서 받게 — 수량이 품셈 갈래를 고를 수 있게 됨
데스크탑 창 보고(2026-09-08): 실무 프로젝트 구조물의 **상세 옵션이 통째로 비어 있음** —
`back_len_cm`(뒷길이) · `stone_cm`(돌규격) · `form`(옹벽 형식). 그래서 B08 수량이 품셈 갈래를
못 골라 **금액이 0 으로 남음**(13-4 돌쌓기 뒷길이 · 13-6 큰돌쌓기 직경 · 12-3 옹벽 형식).
**까닭 — 아무도 안 받고 있었음.** 레지스트리가 그 칸들을 `phase: "detail"` 로 두고
「B05 는 유무·종류·위치만, **상세 치수는 B06/B07 에서**」(2026-08-17 사용자 확정)로 갈라 놨는데,
**그 화면이 아직 안 그리고 있었음**. 폼은 한 벌인데 필터(`isB05Option`)가 늘 상세를 숨겼음.
고침 — 폼이 `includeDetail` 을 받아 **B06 에서만** 상세 칸까지 그림(B05 는 종전 그대로).
새 결정이 아니라 **2026-08-17 확정을 이제 구현한 것**임.
⚠ **기본값 없는 필수 항목은 빈 칸(「— 선택 —」)으로 연다** — 첫 항목을 슬쩍 고르면 근거 없는
값(뒷길이 25㎝ 같은)이 수량·단가로 흘러감. 설계자가 고르게 둠.
**큰돌쌓기에 「쌓기 방식」(메쌓기·찰쌓기) 옵션 신설** — 품셈이 13-6-1 메 / 13-6-2 찰로 갈라
두는데 구분 칸이 없었음(돌쌓기는 이미 타입이 갈려 있음). 기본값 없음·필수·상세.
실화면 확인(용화 5601e828 · B06) — C군 → 큰돌쌓기 고르면 **돌규격·쌓기 방식** 칸이 빈 채로 뜸.
돌규격 `60~80` · 쌓기 방식 `찰쌓기` 로 한 건 넣고 [저장] → 정본 `structures.json` 에
`{"stone_cm": "60~80", "bond": "찰쌓기", …}` 로 남는 것 확인. 시험분은 지우고 저장해
**원래 7건**으로 되돌림.
시험 5건 — B06 만 상세를 받는지 · B05 는 종전대로인지 · 기본값 없는 필수는 빈 칸인지 ·
큰돌쌓기 메/찰 구분 · **규격 축이 타입마다 다른지**(큰돌쌓기=직경, 돌쌓기=뒷길이 — 데스크탑이
섞여 있던 결함을 잡은 자리). 전체 501 passed · 17 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,13 @@ export interface StructureOptionField {
|
||||
phase?: "b05" | "detail";
|
||||
}
|
||||
|
||||
/** 구조물 배치 폼을 어느 화면이 쓰는가 — B05 는 유무·종류·위치만, **B06/B07 은 상세
|
||||
* 치수까지** 받는다(2026-08-17 사용자 확정). 부르는 쪽이 정한다. */
|
||||
export interface StructuresSectionOptions {
|
||||
/** 참이면 `phase: "detail"` 옵션(뒷길이·돌규격·형식 …)도 폼에 그린다. */
|
||||
includeDetail?: boolean;
|
||||
}
|
||||
|
||||
/** B05 배치 폼에 그릴 옵션인가 — 상세(detail)는 B06/B07 몫이라 숨긴다. */
|
||||
export function isB05Option(option: StructureOptionField): boolean {
|
||||
return (option.phase ?? "b05") !== "detail";
|
||||
|
||||
@@ -956,6 +956,15 @@
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "bond",
|
||||
"label": "쌓기 방식",
|
||||
"input": "select",
|
||||
"choices": ["메쌓기", "찰쌓기"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "side",
|
||||
"label": "설치 측",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
isB05Option,
|
||||
structureAnchorM,
|
||||
type StructureInstance,
|
||||
type StructuresSectionOptions,
|
||||
type StructurePlacement,
|
||||
type StructureType,
|
||||
} from "./B05_Profile_Api_Structures";
|
||||
@@ -49,7 +50,10 @@ import type {
|
||||
StructuresSection,
|
||||
} from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
|
||||
export function createStructuresSection(callbacks: StructuresCallbacks): StructuresSection {
|
||||
export function createStructuresSection(
|
||||
callbacks: StructuresCallbacks,
|
||||
sectionOptions: StructuresSectionOptions = {},
|
||||
): StructuresSection {
|
||||
// 폼 뼈대는 전용 조립기가 세운다(2026-09-03 · 700줄 제한) — 여기서는 값·검증·저장만.
|
||||
const form = buildStructuresForm({
|
||||
getInterval: () => callbacks.getInterval(),
|
||||
@@ -294,8 +298,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
syncRangeDisplay();
|
||||
}
|
||||
|
||||
/** 타입의 옵션 스키마대로 입력 칸을 다시 그린다 — 상세(detail) 옵션은 B06/B07
|
||||
* 몫이라 그리지 않는다(B05 = 유무·종류·위치 단계, 2026-08-17 사용자 확정). */
|
||||
/** 타입의 옵션 스키마대로 입력 칸을 다시 그린다. 상세(detail)는 `includeDetail` 인 화면
|
||||
* (B06/B07)에서만 — 안 받으면 뒷길이·돌규격·형식이 비어 **수량이 갈래를 못 고른다**. */
|
||||
function renderOptionFields(values: Record<string, string | number> = {}): void {
|
||||
const type = currentType();
|
||||
// 여기도 배열을 갈아끼우지 않는다 — 조각들이 참조로 받아 두므로 새 배열로
|
||||
@@ -303,7 +307,9 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
optionInputs.length = 0;
|
||||
optionRow.replaceChildren();
|
||||
// 서브폼이 담당하는 타입(계곡 통과 시설·독립 기슭막이)은 여기서 그리지 않는다.
|
||||
const visible = type && !facilityFormKind(type) ? type.options.filter(isB05Option) : [];
|
||||
const all = sectionOptions.includeDetail === true;
|
||||
const visible =
|
||||
type && !facilityFormKind(type) ? type.options.filter((o) => all || isB05Option(o)) : [];
|
||||
if (!visible.length) {
|
||||
optionRow.hidden = true;
|
||||
syncOptionLock();
|
||||
@@ -317,8 +323,11 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
// 빈("선택하세요") 항목은 두지 않는다 — 첫 항목이 곧 기본값이고, 기본값은
|
||||
// 구조물별로 레지스트리에서 지정한다(2026-08-17 사용자 지시 1).
|
||||
const choices = option.choices.map((choice) => [choice, choice] as [string, string]);
|
||||
// 기본값 없는 필수 항목은 **빈 칸으로** — 첫 항목을 슬쩍 고르면 근거 없는 값이 나간다.
|
||||
const mustPick = option.required === true && (option.default ?? "") === "";
|
||||
if (mustPick) choices.unshift(["", "— 선택 —"]);
|
||||
input = select(choices);
|
||||
input.value = String(preset || (option.choices[0] ?? ""));
|
||||
input.value = String(preset || (mustPick ? "" : (option.choices[0] ?? "")));
|
||||
} else if (option.input === "number") {
|
||||
input = numberInput("0.1", "0");
|
||||
input.value = String(preset ?? "");
|
||||
|
||||
Reference in New Issue
Block a user