feat(M02): 관리 화면 「구조물 조합」 — 나무가 주 · 표는 참조표 · 비고 · 전구간 · 합계 숨김(PLAN 26-7a~e)

- 화면 이름 「구조물집계표」 · 「구조물 집계 양식」 → 「구조물 조합」(좌측 줄 · 상세 제목 · 안내글) · 파일 이름 · 양식 키 · API 길 그대로
- 좌측 패널 「항목 나무」 컨테이너 없앰 — 페이지가 나무를 만들어 편집 칸 왼쪽(aside)에 붙임 · 「구조물 조합」일 때만 보임 · 오른쪽 클릭 · 찾기 · 표와 서로 고름 · 고치기 전 저장 물음 그대로
- 표 부품 lockHead — 열 더하기 · 분류N 더하기 · 열 지우기 단추 없음 · 머리 · 층 이름 고치기 없음(단위 · 일위대가 · 조회 후 선택은 그대로)
- 비고 열 · 전구간 줄 · 합계 줄은 보일 문서에서만 뺌(comboView) · [저장] 때 저장본 것을 다시 끼움(comboMerge) · 양식 파일 · M02_Table_Fill 그대로
- 서버 — 화면 표 [저장](store.write screen=True)은 있던 항목의 경로 · 층 이름 · 빈 폴더를 저장본 그대로(tree.keep_paths · 브라우저 머리 안 받음) · 머리는 경로에서 다시 만듦 · 나무 고침 줄기 그대로
- 나무 같은 이름 409 는 「이미 있는 항목」 까닭 그대로 보임(판 다름 안내와 가름)
- 시험 test_m02_combo_screen 3(숨김 · 되끼움 Node · 잠금 · 나무 자리) · test_m02_tree 표 저장 약속 새로 · 이름 · 잠금 기대값 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
This commit is contained in:
2026-09-30 10:39:59 +09:00
co-authored by Claude Opus 5.5
parent 551593d040
commit b4100acc6d
19 changed files with 306 additions and 61 deletions
@@ -17,6 +17,8 @@ import {
type StructureDoc,
} from "./M02_MasterTemplete_Api_Fetch";
import {
comboMerge,
comboView,
createStructurePanel,
pillsFor,
STRUCTURE_TABLE,
@@ -103,9 +105,11 @@ export function canEdit(layer: Layer, isAdmin: boolean): boolean {
return layer === "project" || isAdmin;
}
/** 페이지가 끼우는 것 — `top` = 편집 칸 맨 위 막대(회사용 읽기만 안내) · `onColumn` = 집계표에서 고른 열 */
/** 페이지가 끼우는 것 — `top` = 편집 칸 맨 위 막대(회사용 읽기만 안내) · `onColumn` = 집계표에서 고른 열 ·
* `aside` = 「구조물 조합」을 열 때만 편집 칸 왼쪽에 보임(항목 나무 · PLAN 26-7c) */
export interface MainExtras {
top?: HTMLElement;
aside?: HTMLElement;
onColumn?: (column: string | null) => void;
}
@@ -125,7 +129,7 @@ export function createMain(
const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } });
const host = el("div", { className: "m02-main__host" });
const extra = el("div", { className: "m02-main__extra" }); // 표 밖 형제 자리 — 구조물 설계 컨테이너
const root = el("div", {
const column = el("div", {
className: "m02-main",
children: [
...(extras.top ? [extras.top] : []),
@@ -135,6 +139,15 @@ export function createMain(
extra,
],
});
// 왼쪽 나무(주) · 오른쪽 편집 칸 — 나무는 「구조물 조합」일 때만(`showAside`)
const root = extras.aside
? el("div", { className: "m02-shell", children: [extras.aside, column] })
: column;
const isCombo = (at: Selection | null): boolean =>
!!at && at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project";
const showAside = (): void => {
if (extras.aside) extras.aside.hidden = !isCombo(sel);
};
let sel: Selection | null = null;
let version = "";
@@ -360,9 +373,11 @@ export function createMain(
// 그 뒤 .열 을 그대로 읽으면 죽는다(2026-09-28 사용자 보고 — 열 고른 뒤 CAD
// 불러오기 오류). getDoc() 뒤에도 옵셔널 체이닝을 이어야 한다.
const columns = (): StructureColumn[] => sheet?.getDoc()?.열 ?? [];
editor = create(host, doc, {
// 「구조물 조합」 = 나무를 따라오는 참조표(26-7b · d) — 비고 · 전구간 · 합계 숨김 · 열 · 머리는 나무만
editor = create(host, struct ? comboView(doc) : doc, {
mode: at.layer === "project" ? "project" : "master",
readOnly,
lockHead: !!struct,
onChange: () => (dirty = !readOnly),
...(struct
? {
@@ -382,7 +397,11 @@ export function createMain(
}
: {}),
});
if (struct) sheet = editor as SheetEditorHandle;
if (!struct) return;
const shown = editor as SheetEditorHandle;
sheet = shown;
// [저장]은 저장본의 숨긴 것을 다시 끼운 문서(서버가 머리를 경로에서 다시 만듦 · 26-7e)
editor = { getDoc: () => comboMerge(loaded, shown.getDoc()), destroy: shown.destroy };
return;
}
if (at.kind === "area" || at.kind === "material") {
@@ -421,11 +440,12 @@ export function createMain(
sel = next;
onOpened?.();
bar();
showAside();
if (!next) {
title.textContent = L("M02_PickTemplate");
return empty("");
}
title.textContent = next.label ?? next.name;
title.textContent = isCombo(next) ? L("M02_KindTable") : (next.label ?? next.name); // 26-7a
try {
const got = await readTemplate(next.layer, next.kind, next.name, next.projectId);
if (mine !== seq) return;
@@ -501,6 +521,7 @@ export function createMain(
sel = null;
onOpened?.(); // 좌측 양식 · 열 줄 고름 지움
bar();
showAside();
title.textContent = label;
host.replaceChildren(node);
return (d) => (dirty = d);
@@ -544,6 +565,7 @@ export function createMain(
if (sel && (await confirmLeave())) void open(sel);
});
bar();
showAside();
empty("");
return { root, open, current: () => sel, confirmLeave, showView, openTable, settle };
}