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:
@@ -10,7 +10,8 @@ import { el } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createWorkflowOverlays } from "@ui/ui_template_overlay";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import { createMain } from "./M02_MasterTemplete_UI_Main";
|
||||
import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure";
|
||||
import { createMain, type Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
import { buildSide } from "./M02_MasterTemplete_UI_Side";
|
||||
import "./M02_MasterTemplete_UI_Style.css";
|
||||
|
||||
@@ -22,9 +23,16 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<void>
|
||||
() => void side.refresh(),
|
||||
() => side.paint(),
|
||||
);
|
||||
// 첫 화면 · 닫은 뒤(양식 지움 · 자재 조합 상세 닫음) = 구조물집계표 — 빈 화면 없음
|
||||
const home: Selection = {
|
||||
layer: "system",
|
||||
projectId: null,
|
||||
kind: "table",
|
||||
name: STRUCTURE_TABLE,
|
||||
};
|
||||
const side = buildSide({
|
||||
isAdmin,
|
||||
onOpen: (sel) => void main.open(sel),
|
||||
onOpen: (sel) => void main.open(sel ?? home),
|
||||
getOpen: main.current,
|
||||
confirmLeave: main.confirmLeave,
|
||||
showView: main.showView,
|
||||
@@ -46,4 +54,5 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<void>
|
||||
);
|
||||
root.innerHTML = "";
|
||||
root.append(layout);
|
||||
void main.open(home);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
/* 구조물 집계 양식 하위 컨테이너(구조물 도면 · 면적산출 · 재료산출 · 자재 조합) — 들여 씀 */
|
||||
.m02-side__summary > .ui-sidebar-card {
|
||||
margin-left: var(--spacing-8);
|
||||
}
|
||||
|
||||
.m02-side__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -140,12 +140,24 @@ def test_미리보기_CAD_패널은_숨기지_않고_접힌_채로_엶():
|
||||
assert "isInspectorCollapsed()" in _text(cad / "components/Toolbar.tsx")
|
||||
|
||||
|
||||
def test_좌측_새로_본떠_지우기는_표_도면_컨테이너_안에_하나씩():
|
||||
def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우기는_도면_양식만():
|
||||
side = _text(SIDE)
|
||||
assert 'actionRow("table")' in side and 'actionRow("drawing")' in side
|
||||
assert 'actionRow("table")' not in side and 'actionRow("drawing")' in side
|
||||
assert "createSelectField" not in side # 새로 = 종류 고르기 없이 이름만
|
||||
assert "cur?.종류 !== kind" in side # 본떠 · 지우기 = 그 컨테이너에서 고른 것이 있을 때만
|
||||
assert "양식: name" in side.split("function copyTemplate")[1] # 복사본 머리 이름 = 새 이름
|
||||
assert "i.이름 === STRUCTURE_TABLE" in side # 표는 구조물집계표 한 줄만
|
||||
# 구조물 도면 · 면적산출 · 재료산출 · 자재 조합 = 구조물 집계 양식 컨테이너 안(같이 접힘)
|
||||
summary = side.split("const summary = section(")[1].split(");")[0]
|
||||
assert "COLUMN_KINDS.map" in summary and "matsetSection" in summary
|
||||
assert 'M02_KindTable: ["구조물 집계 양식"' in _text(
|
||||
ROOT / "ui_template" / "ui_template_locale_m2.ts"
|
||||
)
|
||||
|
||||
|
||||
def test_첫_화면과_닫은_뒤는_구조물집계표():
|
||||
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
||||
assert re.search(r'kind: "table",\s*name: STRUCTURE_TABLE', page)
|
||||
assert "main.open(sel ?? home)" in page and "void main.open(home);" in page
|
||||
|
||||
|
||||
def test_좌측_고름_표시는_열린_것_한_줄뿐():
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
export const ui_locales_m2 = {
|
||||
B01_Dashboard_MasterTemplate: ["마스터 템플릿", "Master Templates"],
|
||||
M02_Title: ["마스터 템플릿", "Master Templates"],
|
||||
M02_KindTable: ["표 양식", "Table templates"],
|
||||
M02_KindTable: ["구조물 집계 양식", "Structure summary templates"],
|
||||
M02_KindDrawing: ["도면 양식", "Drawing templates"],
|
||||
M02_KindStructure: ["구조물 도면", "Structure drawings"],
|
||||
M02_KindArea: ["면적산출", "Area takeoff"],
|
||||
|
||||
Reference in New Issue
Block a user