feat(M02): 「구조물 조합」 제목 줄 오른쪽 끝 「새 항목」 (PLAN 30-5 구조물 쪽)

- 쓸 수 있는 층만 · 구조물 조합일 때만 보임 · 나무 「항목 추가」와 같은 창 · 고른 폴더 안(없으면 맨 위)
- 공용 모양 newItemButton · m02-main__new(일위대가 「새 조합」이 같은 자리 · 모양을 씀)
- 시험 한 건 · 머리 줄 시험 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:23:39 +09:00
co-authored by Claude Opus 5.5
parent 62073aac1b
commit dc725012ed
7 changed files with 63 additions and 7 deletions
@@ -107,6 +107,8 @@ export interface TreePanelHandle {
refresh: () => Promise<void>;
/** 표에서 고른 열을 나무에 표시(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 };
}
@@ -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;
@@ -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<Mounte
{
top: readOnlyBar?.root,
search: tree.search,
// 「새 항목」(30-5) — 제목 줄 오른쪽 끝 · 쓸 수 있는 층만 · 나무 「항목 추가」와 같은 창
...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => tree.newItem()) } : {}),
onColumn: (column) => {
readOnlyBar?.setColumn(column);
tree.select(column);
@@ -226,4 +226,13 @@
.m02-main__head + .m02-tree__search {
margin-bottom: var(--spacing-8);
}
}
/* 상세 제목 줄 오른쪽 끝 「새 항목」 · 「새 조합」(PLAN 30-5 · 공용) — 늘어나는 칸이 없어도 끝으로 */
.m02-main__new {
margin-left: auto;
}
.m02-main__new[hidden] {
display: none;
}
+22
View File
@@ -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"
)
+1 -1
View File
@@ -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
+1
View File
@@ -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})"],