feat(M02): 집계표 카드 = 도면 정보 · 면적산출 · 재료산출 · 편집 화면 알약 막대

- 집계표 아래 카드 셋(보기 + [수정] → 전체 편집) · 번호 = 도번 · 면적번 · 재료번
- 면적산출 · 재료산출 줄 조회 후 선택 = 다른 열 것 복사해 가져오기(copyArea · copyMaterial · 덮어쓰기 확인)
- 면적산출 · 재료산출은 표(스프레드시트) 편집 화면으로 엶(좌측 줄 · 카드 [수정] 둘 다 · 옛 CAD 로 뜨던 것 고침)
- 알약 막대 — 면적 = 그 열 일위대가 입력값(fetchComboInputs · 없으면 안내 글) · 재료 = 자재DB 찾기(searchMaterials)
- 옛 산출근거 이름(표번 · copyBasis) 안 씀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Kyw9qnkJ94pC1A4PfpDtA
This commit is contained in:
2026-09-28 12:16:25 +09:00
co-authored by Claude Opus 5.5
parent 9ae91ea5da
commit b60c10e73e
4 changed files with 194 additions and 115 deletions
@@ -18,6 +18,7 @@ import {
} from "./M02_MasterTemplete_Api_Fetch";
import {
createStructurePanel,
pillsFor,
STRUCTURE_TABLE,
type StructureColumn,
type StructurePanelHandle,
@@ -43,6 +44,12 @@ interface SheetEditorHandle extends EditorHandle {
}
type Mods = Record<string, () => Promise<unknown>>;
/** 열 id 로 여는 문서의 번호 칸 */
const NUMBER_FIELD: Partial<Record<Kind, string>> = {
structure: "도번",
area: "면적번",
material: "재료번",
};
// 표 부품(sub3) · 도면 부품(sub2) — 내보낸 이름으로 찾음
// 표는 진입 파일만 — 폴더 통째(`*.ts`)면 분수 모듈이 따로 늦게 받는 입구가 되어 번들러 도우미를
// 달고, 그 도우미가 든 큰 조각(compass)까지 스프레드시트가 끌어옴
@@ -138,7 +145,7 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
slot.replaceChildren();
};
/** 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 미리보기 · [도면 수정] · 상세 산출근거 */
/** 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 · 재료산출 카드 */
function buildPanel(): StructurePanelHandle {
const made = createStructurePanel({
isAdmin,
@@ -161,8 +168,8 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
"createSheet",
);
if (!create) return empty(L("M02_NoSheet"));
// 구조물집계표(시스템 층)만 — 도면 · 산출근거 줄에 도번 · 표번 · 칸을 고르면 아래 컨테이너 ·
// 고칠 수 있으면 일위대가 · 도면 · 산출근거 칸은 조회 후 선택
// 구조물집계표(시스템 층)만 — 도면 · 면적산출 · 재료산출 줄에 번호 · 칸을 고르면 아래 컨테이너 ·
// 고칠 수 있으면 일위대가 · 도면 · 면적산출 · 재료산출 칸은 조회 후 선택
const struct = at.layer === "system" && at.name === STRUCTURE_TABLE ? buildPanel() : null;
if (struct) await struct.reload();
panel = struct;
@@ -177,7 +184,8 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
...(struct
? {
drawingLabel: (colId: string) => struct.numbers()[colId]?.도번 ?? "",
basisLabel: (colId: string) => struct.numbers()[colId]?.표번 ?? "",
areaLabel: (colId: string) => struct.numbers()[colId]?.면적번 ?? "",
materialLabel: (colId: string) => struct.numbers()[colId]?.재료번 ?? "",
onSelect: (s: { colId: string | null }) => struct.show(s.colId, columns()),
...(readOnly
? {}
@@ -191,15 +199,21 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
if (struct) sheet = editor as SheetEditorHandle;
return;
}
if (at.kind === "basis") {
// 산출근거 = 구조물집계표 열마다 한 스프레드시트 — 페이지 번들에 안 섞이게 이 화면을 열 때만
// `import()` 로 받음(계약 `spreadsheet_types.ts` 머리)
if (at.kind === "area" || at.kind === "material") {
// 면적산출 · 재료산출 = 구조물집계표 열마다 한 스프레드시트 — 페이지 번들에 안 섞이게 이 화면을
// 열 때만 `import()` 로 받음(계약 `spreadsheet_types.ts` 머리)
// 위 알약 막대(sub7 `links`) — 면적 = 그 열 일위대가 입력값 · 재료 = 자재DB 찾기(`pillsFor`)
try {
const { createSpreadsheet } = await import("../A00_Common/spreadsheet/spreadsheet");
const [{ createSpreadsheet }, pills] = await Promise.all([
import("../A00_Common/spreadsheet/spreadsheet"),
pillsFor(at.kind, at.name),
]);
if (pills.notice) showNotice([pills.notice]);
editor = createSpreadsheet(host, doc as Parameters<typeof createSpreadsheet>[1], {
readOnly,
tabs: false, // 산출근거 = 시트 하나 · 탭 줄 없음
tabs: false, // 시트 하나 · 탭 줄 없음
onChange: () => (dirty = !readOnly),
links: pills.links,
});
} catch {
empty(L("M02_NoSpreadsheet"));
@@ -211,7 +225,7 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
"mountDrawingTemplate",
);
if (mountDrawing) {
// 구조물 도면은 CAD 문서가 `도면` 칸에 들어 있음 — 도번 · 산출근거는 안 건드림
// 구조물 도면은 CAD 문서가 `도면` 칸에 들어 있음 — 도번은 안 건드림
const cad = at.kind === "structure" ? (doc as StructureDoc).도면 : doc;
editor = mountDrawing(host, cad, {
onSave: () => void doSave(),
@@ -253,14 +267,10 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
)
: got.문서;
if (mine !== seq) return;
// 구조물 도면 · 산출근거는 열 id 로 열리니 머리 맨 앞에 도번 · 표번(「SD010101 돌기슭막이 …」)
if (next.kind === "structure") {
const number = (got.문서 as StructureDoc)?.도번;
if (number) title.textContent = `${number} ${next.label ?? next.name}`;
} else if (next.kind === "basis") {
const number = (got.문서 as { 표번?: string })?.표번;
if (number) title.textContent = `${number} ${next.label ?? next.name}`;
}
// 구조물 도면 · 면적산출 · 재료산출은 열 id 로 열리니 머리 맨 앞에 번호(「SD01010100 돌기슭막이 …」)
const field = NUMBER_FIELD[next.kind];
const number = field && (got.문서 as Record<string, unknown> | null)?.[field];
if (number) title.textContent = `${String(number)} ${next.label ?? next.name}`;
await mount(shown, next);
} catch (error) {
if (mine !== seq) return;
@@ -275,12 +285,12 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
const at = sel;
try {
const edited = editor ? await editor.getDoc() : loaded;
// 구조물 도면 저장은 `도면` 칸만 갈아 끼움 — 도번 · 산출근거는 읽은 그대로.
// 산출근거는 스프레드시트 문서로 통째 갈아 끼우되 표번은 계약 밖(엔진이 모름)이라 옮겨 붙임.
// 구조물 도면 저장은 `도면` 칸만 갈아 끼움 — 도번은 읽은 그대로.
// 면적산출 · 재료산출은 스프레드시트 문서로 통째 갈아 끼우되 번호는 계약 밖(엔진이 모름)이라 옮겨 붙임.
const doc =
at.kind === "structure" && loaded
? { ...(loaded as StructureDoc), 도면: edited as StructureDoc["도면"] }
: at.kind === "basis" && loaded
: (at.kind === "area" || at.kind === "material") && loaded
? { ...(loaded as Record<string, unknown>), ...(edited as Record<string, unknown>) }
: edited;
const info = await saveTemplate(at.layer, at.kind, at.name, at.projectId, version, doc);