feat(M02): 좌측 패널 재구성 — 구조물 집계 양식 아래 하위 넷 · 첫 화면 구조물집계표

- 「표 양식」 → 「구조물 집계 양식」 · 구조물집계표 줄 하나만 · 새로 · 본떠 만들기 · 지우기 단추 삭제
- 구조물 도면 · 면적산출 · 재료산출 · 자재 조합 컨테이너를 그 아래 하위로(들여 씀 · 같이 접힘) · 도면 양식은 별도 최상위
- 들어오면 구조물집계표를 엶 · 양식 지움 · 자재 조합 상세 닫음 뒤에도 빈 화면 대신 구조물집계표
- 표 새 양식 틀 · 표 복사 머리 이름 바꿈(이제 안 쓰는 길) 걷어냄 · 시험 고침

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 08:40:09 +09:00
co-authored by Claude Opus 5.5
parent 7cac596afb
commit 155d1eefbe
5 changed files with 62 additions and 40 deletions
@@ -1,9 +1,10 @@
/* =============================================================================
* M02_MasterTemplete_UI_Side.ts
* 마스터 템플릿 좌측 패널 — 시스템 양식 목록(표/도면/구조물 도면/면적산출/재료산출) · 새로 · 본떠 · 지우기.
* 서버 길은 계약 `6_계약.md`. 구조물 도면 · 면적산출 · 재료산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 —
* 마스터 템플릿 좌측 패널 — 「구조물 집계 양식」(구조물집계표 줄 하나 + 하위 구조물 도면 · 면적산출 ·
* 재료산출 · 자재 조합) · 「도면 양식」(새로 · 본떠 · 지우기). 서버 길은 계약 `6_계약.md`.
* 구조물 도면 · 면적산출 · 재료산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 —
* 서버가 미리 만들어 둠(도번 SD · 면적번 SA · 재료번 SM + 같은 8자리) ·
* 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」). 맨 아래 「자재 조합」 컨테이너는 `M02_MasterTemplete_Matset.ts`.
* 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」). 「자재 조합」 컨테이너는 `M02_MasterTemplete_Matset.ts`.
* ========================================================================== */
import {
@@ -48,27 +49,14 @@ const COLUMN_KINDS: { kind: ColumnKind; key: keyof StructureNumbers; label: () =
{ kind: "area", key: "면적번", label: () => L("M02_KindArea") },
{ kind: "material", key: "재료번", label: () => L("M02_KindMaterial") },
];
/** 새 양식의 첫 문서 — 표는 `3_표양식.md` 4장 틀 · 도면은 빈 웹캐드 문서 */
function stubDoc(kind: Kind, name: string): unknown {
return kind === "table"
? {
양식: name,
종류: "표",
판: 1,
층: ["종류", "공법", "규격"],
열: [{ id: "sta", 머리: ["측점", null, null], 단위: null, 꼴: "글", 고정: true }],
줄: [],
합계줄: [],
보기: {},
}
: {
format: 6,
entities: [],
// 기본 도면층 하나 — 없으면 B07 CAD 가 못 엶("Cannot read properties of
// undefined (reading 'length')", 2026-09-27 실측)
layers: [{ id: "기본", name: "기본", isVisible: true, isLocked: false }],
};
}
/** 새 도면 양식의 첫 문서 — 빈 웹캐드 문서 */
const STUB_DRAWING = {
format: 6,
entities: [],
// 기본 도면층 하나 — 없으면 B07 CAD 가 못 엶("Cannot read properties of
// undefined (reading 'length')", 2026-09-27 실측)
layers: [{ id: "기본", name: "기본", isVisible: true, isLocked: false }],
};
export interface SideOptions {
isAdmin: boolean;
@@ -116,7 +104,7 @@ export function buildSide(opt: SideOptions): SideHandle {
const columnBodies = COLUMN_KINDS.map(() =>
el("div", { className: "m02-side__items ui-sidebar-list" }),
);
/** 새로 · 본떠 만들기 · 지우기 — 표 · 도면 컨테이너 안에 하나씩(그 종류로 동작) · 관리자만 */
/** 새로 · 본떠 만들기 · 지우기 — 도면 양식 컨테이너 안(구조물 집계 양식은 집계표 하나라 없음) · 관리자만 */
const editButtons = new Map<Kind, { copy: HTMLButtonElement; del: HTMLButtonElement }>();
const actionRow = (kind: Kind): HTMLElement[] => {
if (!canEdit(layer, opt.isAdmin)) return [];
@@ -141,13 +129,20 @@ export function buildSide(opt: SideOptions): SideHandle {
matsetSection.querySelector(".ui-collapsible__title")?.addEventListener("click", () => {
if (matsetSection.classList.contains("is-collapsed")) void matset.refresh();
});
// 구조물 집계 양식 — 집계표 줄 아래 하위 컨테이너 넷(들여 · 같이 접힘) · 처음부터 펼침(집계표가 열려 있음)
const summary = section(
L("M02_KindTable"),
bodies.get("table")!,
...COLUMN_KINDS.map((k, i) => section(k.label(), columnBodies[i])),
matsetSection,
);
summary.classList.remove("is-collapsed");
summary.classList.add("m02-side__summary");
const listHost = el("div", {
className: "m02-side__lists ui-sidebar-panel",
children: [
section(L("M02_KindTable"), bodies.get("table")!, ...actionRow("table")),
summary,
section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing")),
...COLUMN_KINDS.map((k, i) => section(k.label(), columnBodies[i])),
matsetSection,
],
});
attachCollapsible(listHost);
@@ -169,7 +164,10 @@ export function buildSide(opt: SideOptions): SideHandle {
function paintList(): void {
for (const [kind, body] of bodies) {
const mine = items.filter((i) => i.종류 === kind);
// 표는 구조물집계표 한 줄만(다른 표 양식은 안 보임)
const mine = items.filter(
(i) => i.종류 === kind && (kind !== "table" || i.이름 === STRUCTURE_TABLE),
);
body.replaceChildren(
...(mine.length
? mine.map((info) => {
@@ -265,7 +263,7 @@ export function buildSide(opt: SideOptions): SideHandle {
await Promise.all([refreshStructure(), matset.refresh()]);
}
/* --- 시스템 층 새로 · 본떠 · 지우기(표 · 도면만 — 구조물 도면은 집계표 열이 정함) --- */
/* --- 시스템 층 새로 · 본떠 · 지우기(도면 양식만 — 구조물 도면은 집계표 열이 정함) --- */
const nameDialog = (title: string, onOk: (name: string) => Promise<void>): void => {
const name = createInputField({ label: L("M02_Name"), placeholder: L("M02_Name") });
openModal({
@@ -307,7 +305,7 @@ export function buildSide(opt: SideOptions): SideHandle {
/** 이 컨테이너 종류의 새 양식 — 종류 고르기 없이 이름만 */
function newTemplate(kind: Kind): void {
nameDialog(L("M02_NewTitle"), async (name) => {
await saveTemplate("system", kind, name, null, "", stubDoc(kind, name));
await saveTemplate("system", kind, name, null, "", STUB_DRAWING);
await openCreated(kind, name);
});
}
@@ -316,9 +314,7 @@ export function buildSide(opt: SideOptions): SideHandle {
if (!from) return;
nameDialog(L("M02_CopyTitle"), async (name) => {
const got = await readTemplate("system", from.종류, from.이름, null);
// 표 문서 머리 이름(`양식`)도 새 이름으로 — 안 바꾸면 복사본이 원본 이름을 달고 있음
const doc = from.종류 === "table" ? { ...(got.문서 as object), 양식: name } : got.문서;
await saveTemplate("system", from.종류, name, null, "", doc);
await saveTemplate("system", from.종류, name, null, "", got.문서);
await openCreated(from.종류, name);
});
}