feat(M02): 면적산출 · 재료산출 오른쪽 조합 패널 — 조합 목록 · 이 열에 쓰기 · 조합 고치기

- 오른쪽 안쪽 패널: 공용 사이드바 카드 · 접기(attachCollapsible) · 공용 폭 손잡이(createPanelResizer · 폭 기억 · 최소 280 · 최대 60%) · 공용 접기 손잡이(createWorkflowPanelHandle)
- 「이 열의 조합」 칸: 면적 = 일위대가 조합 줄 + [조합 고치기] · 재료 = 담은 자재 표(옆 판 aside 없앰)
- 조합 목록: 면적 = 일위대가(UA) · 재료 = 자재 조합(UM) · 키 · 이름 · 수 · 본뜬키 · 찾기 · 이 열 조합 강조
- [이 열에 쓰기]: 서버 POST /api/m02/structures/{column}/use-combo(집계표 판 검사 · 그 열 키만 바꿈 · 조합은 그대로) → 알약 다시 받음
- 상태 등록표에 m02-combo-panel-width · collapsed · 시험 더함

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-29 09:04:19 +09:00
co-authored by Claude Opus 5.5
parent acc4408028
commit 1bba901834
12 changed files with 461 additions and 68 deletions
@@ -24,6 +24,7 @@ import {
type StructurePanelHandle,
} from "./M02_MasterTemplete_Structure";
import { buildComboBar } from "./M02_MasterTemplete_Combo";
import { buildComboPanel, type ComboPanel } from "./M02_MasterTemplete_ComboPanel";
import {
buildMaterialPanel,
loadMaterialState,
@@ -109,7 +110,6 @@ export function createMain(
const back = createButton({ label: L("M02_Back"), variant: "ghost", size: "sm" });
const slot = el("div", { className: "m02-main__slot" }); // 도면 양식의 작도 영역 칸이 들어옴
const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } });
const comboSlot = el("div", { className: "m02-main__combo" }); // 면적산출 — 알약 막대 위 조합 줄
const host = el("div", { className: "m02-main__host" });
const extra = el("div", { className: "m02-main__extra" }); // 표 밖 형제 자리 — 구조물 설계 컨테이너
const root = el("div", {
@@ -117,7 +117,6 @@ export function createMain(
children: [
el("div", { className: "m02-main__head", children: [title, back, slot, reload, save] }),
notice,
comboSlot,
host,
extra,
],
@@ -132,6 +131,7 @@ export function createMain(
let sheetHandle: SpreadsheetHandle | null = null;
let matPanel: MaterialPanel | null = null;
let matState: MaterialState | null = null;
let cpanel: ComboPanel | null = null; // 면적산출 · 재료산출 오른쪽 조합 패널(시스템 층)
let seq = 0;
let dirty = false;
@@ -176,11 +176,12 @@ export function createMain(
sheetHandle = null;
matPanel = null;
matState = null;
cpanel?.destroy();
cpanel = null;
panel?.destroy();
panel = null;
extra.replaceChildren();
slot.replaceChildren();
comboSlot.replaceChildren();
};
/** 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 · 재료산출 카드 */
@@ -244,23 +245,26 @@ export function createMain(
editor = handle;
sheetHandle = handle;
if (at.layer !== "system") return;
// 오른쪽 조합 패널 — 한 번 만들고 시트를 다시 붙여도 그대로(늘 시트 뒤)
cpanel ??= buildComboPanel({
kind,
column: at.name,
isAdmin,
onUsed: () => remount(kind, at, readOnly, true),
});
host.append(cpanel.root);
if (kind === "area") {
const bar = await buildComboBar({
column: at.name,
label: at.label ?? at.name,
isAdmin,
beforeEdit: async () => true,
// 알약만 다시 받음 — 저장 안 한 시트 고침은 그대로 옮김
onApplied: async () => {
if (mine !== seq) return;
const kept = handle.getDoc();
editor?.destroy();
editor = null;
host.replaceChildren();
await mountSheet(kept, kind, at, readOnly);
await remount(kind, at, readOnly, false);
await cpanel?.refresh();
},
});
if (mine === seq) comboSlot.replaceChildren(...(bar ? [bar] : []));
if (mine === seq) cpanel?.setMine(bar);
return;
}
if (matPanel || !matState) return;
@@ -269,6 +273,7 @@ export function createMain(
label: at.label ?? at.name,
isAdmin,
state: matState,
onApplied: () => void cpanel?.refresh(),
links: () => sheetHandle?.getDoc().시트.flatMap((s) => s.연결 ?? []) ?? [],
// 옆 판 표에서 고침 — 알약 막대만 이 목록으로 다시 만듦(저장 안 한 시트 고침은 그대로)
onChange: (items) => {
@@ -294,12 +299,31 @@ export function createMain(
dirty = !readOnly;
},
});
host.append(matPanel.root);
cpanel.setMine(matPanel.root);
} catch {
empty(L("M02_NoSpreadsheet"));
}
}
/** 시트만 다시 붙임(알약을 새로 받음) — 저장 안 한 시트 고침은 그대로 옮김 ·
* `fresh` = 이 열 조합이 바뀜(재료는 자재 조합 · 담은 자재 표를 새로 받음) */
async function remount(
kind: "area" | "material",
at: Selection,
readOnly: boolean,
fresh: boolean,
): Promise<void> {
const kept = sheetHandle?.getDoc();
if (sel !== at || !kept) return;
editor?.destroy();
editor = null;
if (fresh && kind === "material") {
matPanel = null;
matState = null;
}
await mountSheet(kept, kind, at, readOnly);
}
async function mount(doc: unknown, at: Selection): Promise<void> {
const readOnly = !canEdit(at.layer, isAdmin);
host.replaceChildren();