From dc725012eda275a20bcaaf29f2cf5eb390546cb1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 19:23:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=E3=80=8C=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=AC=BC=20=EC=A1=B0=ED=95=A9=E3=80=8D=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=EC=A4=84=20=EC=98=A4=EB=A5=B8=EC=AA=BD=20=EB=81=9D=20=E3=80=8C?= =?UTF-8?q?=EC=83=88=20=ED=95=AD=EB=AA=A9=E3=80=8D=20(PLAN=2030-5=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=AA=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 쓸 수 있는 층만 · 구조물 조합일 때만 보임 · 나무 「항목 추가」와 같은 창 · 고른 폴더 안(없으면 맨 위) - 공용 모양 newItemButton · m02-main__new(일위대가 「새 조합」이 같은 자리 · 모양을 씀) - 시험 한 건 · 머리 줄 시험 맞춤 Co-Authored-By: Claude Opus 5.5 --- .../M02_MasterTemplete_TreePanel.ts | 10 ++++++++- .../M02_MasterTemplete_UI_Main.ts | 20 ++++++++++++++--- .../M02_MasterTemplete_UI_Page.ts | 4 +++- .../M02_MasterTemplete_UI_Style.css | 11 +++++++++- resources/tester/test_m02_layer_screen.py | 22 +++++++++++++++++++ resources/tester/test_m02_structure_panel.py | 2 +- ui_template/ui_template_locale_m2.ts | 1 + 7 files changed, 63 insertions(+), 7 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 6955b284..7dba3012 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -107,6 +107,8 @@ export interface TreePanelHandle { refresh: () => Promise; /** 표에서 고른 열을 나무에 표시(null = 지움) */ select: (colId: string | null) => void; + /** 「새 항목」 — 고른 폴더 안(없으면 맨 위)에 항목 추가 창 */ + newItem: () => void; } const why = (error: unknown): string => (error instanceof Error ? error.message : ""); @@ -672,5 +674,11 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { } }); - return { root, search: search.root, add: tools, refresh, select }; + /** 상세 제목 줄 「새 항목」(30-5) — 나무 「항목 추가」와 같은 창 · 고른 폴더(하나)가 있으면 그 안 · 아니면 맨 위 */ + const newItem = (): void => { + const at = anchor && picked.has(anchor) ? targets.get(anchor) : undefined; + void addItem(at?.kind === "folder" ? at.path : []); + }; + + return { root, search: search.root, add: tools, refresh, select, newItem }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 82baccef..4c3e6bdd 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -106,13 +106,23 @@ export function canEdit(layer: Layer, isAdmin: boolean): boolean { } /** 페이지가 끼우는 것 — `top` = 편집 칸 맨 위 막대(회사용 읽기만 안내) · `onColumn` = 집계표에서 고른 열 · - * `search` = 「구조물 조합」을 열 때만 상세 위쪽에 보이는 나무 찾기 칸(나무는 좌측 패널 · PLAN 29-2) */ + * `search` = 「구조물 조합」을 열 때만 상세 위쪽에 보이는 나무 찾기 칸(나무는 좌측 패널 · PLAN 29-2) · + * `newItem` = 「구조물 조합」 제목 줄 오른쪽 끝 「새 항목」(쓸 수 있는 층만 · PLAN 30-5 · 공용 `m02-main__new`) */ export interface MainExtras { top?: HTMLElement; search?: HTMLElement; + newItem?: HTMLElement; onColumn?: (column: string | null) => void; } +/** 상세 제목 줄 오른쪽 끝 「새 …」 단추 — 공용 모양 `m02-main__new`(일위대가 「새 조합」과 같게 · 30-5) */ +export function newItemButton(run: () => void, label = L("M02_TreeNewItem")): HTMLElement { + const button = createButton({ label, variant: "filled", size: "sm" }); + button.classList.add("m02-main__new"); + button.addEventListener("click", run); + return button; +} + /** `onOpened` — 무엇을 열든(좌측 · 카드 [도면 수정] · 다시 읽기) 연 바로 뒤 부름(좌측 고름 표시) */ export function createMain( isAdmin: boolean, @@ -133,7 +143,10 @@ export function createMain( className: "m02-main", children: [ ...(extras.top ? [extras.top] : []), - el("div", { className: "m02-main__head", children: [title, back, slot, reload, save] }), + el("div", { + className: "m02-main__head", + children: [title, back, slot, reload, save, ...(extras.newItem ? [extras.newItem] : [])], + }), ...(extras.search ? [extras.search] : []), notice, host, @@ -143,9 +156,10 @@ export function createMain( const root = column; const isCombo = (at: Selection | null): boolean => !!at && at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project"; - // 찾기 칸은 「구조물 조합」일 때만 + // 찾기 칸 · 「새 항목」은 「구조물 조합」일 때만 const showAside = (): void => { if (extras.search) extras.search.hidden = !isCombo(sel); + if (extras.newItem) extras.newItem.hidden = !isCombo(sel); }; let sel: Selection | null = null; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 2bc95b0c..ae4021c6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -36,7 +36,7 @@ import { createPeopleTree, seesOthers, } from "./M02_MasterTemplete_TreePanel_Roots"; -import { canEdit, createMain, type Selection } from "./M02_MasterTemplete_UI_Main"; +import { canEdit, createMain, newItemButton, type Selection } from "./M02_MasterTemplete_UI_Main"; import { buildSide } from "./M02_MasterTemplete_UI_Side"; import "./M02_MasterTemplete_UI_Style.css"; @@ -129,6 +129,8 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise tree.newItem()) } : {}), onColumn: (column) => { readOnlyBar?.setColumn(column); tree.select(column); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 2931748f..3238b2fe 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -226,4 +226,13 @@ .m02-main__head + .m02-tree__search { margin-bottom: var(--spacing-8); -} \ No newline at end of file +} + +/* 상세 제목 줄 오른쪽 끝 「새 항목」 · 「새 조합」(PLAN 30-5 · 공용) — 늘어나는 칸이 없어도 끝으로 */ +.m02-main__new { + margin-left: auto; +} + +.m02-main__new[hidden] { + display: none; +} diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index e32af4dc..75eb7c0d 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -233,3 +233,25 @@ def test_켜진_층_단추는_목록으로_복사_끌기(): panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts") assert "if (opt.canEdit) {\n button.draggable = true;" in panel assert 'dragging?.some((t) => t.kind === "folder" && dest && isUnder(dest, t.path))' in panel + + +def test_구조물_조합_제목_줄_새_항목(): + """30-5 — 「구조물 조합」 제목 줄 오른쪽 끝 「새 항목」(공용 `m02-main__new` · 쓸 수 있는 층만 · + 구조물 조합일 때만 보임) · 누르면 나무 「항목 추가」와 같은 창(고른 폴더 안 · 없으면 맨 위).""" + main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") + assert "export function newItemButton(" in main and 'classList.add("m02-main__new")' in main + assert ( + "children: [title, back, slot, reload, save, ...(extras.newItem ? [extras.newItem] : [])]" + in main + ) + assert "if (extras.newItem) extras.newItem.hidden = !isCombo(sel);" in main + page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") + assert ( + "...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => tree.newItem()) } : {})" + in page + ) + panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts") + assert 'void addItem(at?.kind === "folder" ? at.path : []);' in panel + assert ".m02-main__new {\n margin-left: auto;" in _text( + M02 / "M02_MasterTemplete_UI_Style.css" + ) diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index 2a7c7d9b..1d252152 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -209,7 +209,7 @@ def test_표_도구_막대에_줄_더하기_줄_지우기_손_입력_열_없음( def test_상세_화면_돌아가기는_집계표로_그_열_고른_채(): main = _text(MAIN) - assert "children: [title, back, slot, reload, save]" in main # 제목 바로 오른쪽 + assert "children: [title, back, slot, reload, save, " in main # 제목 바로 오른쪽 assert "!(sel.kind in NUMBER_FIELD)" in main # 구조물 도면 · 면적산출 · 재료산출만 body = main.split('back.addEventListener("click"')[1] assert "confirmLeave()" in body and "STRUCTURE_TABLE" in body diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 060b7f31..51e61278 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -131,6 +131,7 @@ export const ui_locales_m2 = { "Copied {n} — right-click a folder in a writable layer or press Ctrl+V", ], M02_TreePicked: ["{n}개 고름", "{n} selected"], + M02_TreeNewItem: ["새 항목", "New item"], M02_TreeCopyToMaster: ["마스터로 복사 ({n}개)", "Copy to master ({n})"], M02_TreeCopyToCompany: ["회사로 복사 ({n}개)", "Copy to company ({n})"], M02_TreeCopyToPersonal: ["개인으로 복사 ({n}개)", "Copy to personal ({n})"],