feat(M02): 구조물집계표 넷째 카드 「선택 입력」 — 로직마다 입력 목록 · 자리 고르기(면적 · 재료 · 선택 · 안 씀) · 알약 줄에 뜸 (PLAN 25-2c)

- 카드 = 로직 이름 · 결과단위 묶음 아래 입력 줄(이름 · 설명 · 단위 · 고르기/범위 · 자동값 흐리게) + 자리 상자 · 처음은 모두 안 씀
- 저장 = 집계표 [저장]에 이어 PUT input-links(자동저장 없음) · 읽기만 층은 상자 잠금
- 면적 · 재료로 고른 입력은 그 시트 알약 줄에 「로직 이름 · 입력 이름」 · 자재 모드에서도 [목록에서 빼기] 막음(고정)
- 알약 줄 시험을 합계 줄 아래 자리에 맞춤(25-0 뒤 어긋난 것)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c
This commit is contained in:
2026-09-29 23:18:13 +09:00
co-authored by Claude Sonnet 5.5
parent fe30cdbbd8
commit db1bb9aa87
11 changed files with 728 additions and 11 deletions
@@ -42,6 +42,7 @@ import {
type DrawingTemplateDoc,
type DrawingTemplateHandle,
} from "./M02_MasterTemplete_Drawing";
import { createInputsCard, inputPills, type InputsCardHandle } from "./M02_MasterTemplete_Inputs";
import "./M02_MasterTemplete_Structure.css";
/** 구조물 도면이 걸리는 집계표 이름 — 서버 `M02_MasterTemplete_Store.STRUCTURE_TABLE` 과 같은 글 */
@@ -207,8 +208,12 @@ export async function pillsFor(
kind: SheetKind,
column: string,
): Promise<{ links: LinkOptions; notice?: string }> {
if (kind === "material") return { links: { 항목: [], 모드: "자재", 자재찾기: searchMaterials } };
const none = (notice: string) => ({ links: { 항목: [], 모드: "입력" as const }, notice });
if (kind === "material") {
const own = await inputPills(column, "재료");
return { links: { 항목: own, 모드: "자재", 자재찾기: searchMaterials } };
}
const own = await inputPills(column, "면적"); // 선택 입력 카드가 면적산출로 고른 입력
const none = (notice: string) => ({ links: { 항목: own, 모드: "입력" as const }, notice });
const table = await readTemplate(getActiveLayer(), "table", STRUCTURE_TABLE, null).catch(
() => null,
);
@@ -217,7 +222,7 @@ export async function pillsFor(
if (!combo) return none(TEXT.noPrice);
try {
const got = await fetchComboInputs(combo);
const 항목 = got.물량.map((q) => ({ 키: q.키, 이름: q.이름, 단위: q.단위 }));
const 항목 = [...got.물량.map((q) => ({ 키: q.키, 이름: q.이름, 단위: q.단위 })), ...own];
return 항목.length ? { links: { 항목, 모드: "입력" } } : none(fill(TEXT.noInputs, combo));
} catch (error) {
return none(fill(TEXT.inputsFailed, why(error)));
@@ -232,6 +237,8 @@ export interface StructurePanelOptions {
onEdit: (column: string, label: string, kind: EditKind) => void;
/** 집계표에 저장 안 한 고침이 있으면 버릴지 물음 — 취소면 false */
confirmLeave: () => Promise<boolean>;
/** 「선택 입력」 카드에서 자리를 골랐음 — 저장 안 한 고침 표시 */
onDirty?: () => void;
}
export interface StructurePanelHandle {
@@ -240,6 +247,8 @@ export interface StructurePanelHandle {
numbers: () => ColumnNumbers;
/** 번호 목록 다시 받기 */
reload: () => Promise<void>;
/** 「선택 입력」 카드에서 고른 자리를 서버에 저장 — 집계표 [저장] 뒤에 부름(실패하면 던짐) */
saveInputs: () => Promise<void>;
/** 고른 열이 바뀜 — 같은 열이면 이름만 새로(CAD 를 다시 안 띄움) */
show: (colId: string | null, columns: StructureColumn[]) => void;
/** 보던 열을 다시 읽음 — 집계표 [저장] 뒤(서버가 새 열 파일 · 번호를 만듦) */
@@ -311,11 +320,15 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
};
};
const sheets = [sheetCard("area"), sheetCard("material")];
const inputs: InputsCardHandle = createInputsCard({
isAdmin: opts.isAdmin,
onDirty: () => opts.onDirty?.(),
});
const root = el("div", {
className: "m02-struct ui-sidebar-panel",
attrs: { hidden: "" },
children: [infoCard, ...sheets.map((c) => c.root)],
children: [infoCard, ...sheets.map((c) => c.root), inputs.root],
});
attachCollapsible(root);
@@ -397,6 +410,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
root.hidden = true;
column = null;
doc = null;
void inputs.show(null, null);
return;
}
root.hidden = false;
@@ -427,6 +441,8 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
c.number = String(book?.[PARTS[c.kind].field] ?? "");
});
paintBody(mine);
// 집계표에 지금 고른 일위대가(저장 전일 수 있음) — 카드가 「먼저 저장」 안내를 고름
void inputs.show(col.id, (col as { 일위대가?: string | null }).일위대가 ?? null);
}
/** 글 열(NO · 측점 · 비고)은 구조물이 아니라 컨테이너를 닫음 */
@@ -516,7 +532,9 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
numbers: () => numbers,
reload: async () => {
numbers = await fetchStructureNumbers().catch(() => ({}));
inputs.invalidate(); // 새 열 · 바뀐 일위대가를 서버 항목 목록에서 다시 찾게
},
saveInputs: () => inputs.save(),
show,
refresh: (columns) => {
const at = column;
@@ -527,6 +545,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
destroy: () => {
seq += 1;
drop();
inputs.destroy();
root.remove();
},
};