From c1ad11e869abc37aef73e2defe030a225e2bc93b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 18:15:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=EC=A2=8C=EC=B8=A1=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=EC=9D=B4=EB=A6=84=20=EB=8B=A8=EC=B6=94=20=C2=B7=20?= =?UTF-8?q?=EB=82=98=EB=AC=B4=20=EB=BF=8C=EB=A6=AC=20=EC=A4=84=20=EC=97=86?= =?UTF-8?q?=EC=95=B0=20=C2=B7=20=E3=80=8C+=E3=80=8D=20=EB=8D=94=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=EC=B0=BE=EA=B8=B0=20=ED=95=98=EB=82=98(AN?= =?UTF-8?q?D)=20(PLAN=2029-6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 「일위대가 조합」 · 「구조물 조합」 컨테이너 안 같은 이름 줄 단추 없앰 — 제목을 펼치면 그 화면을 엶 - 나무 맨 위 뿌리 줄 없앰 — 폴더 · 항목부터 · 맨 위 더하기는 빈 자리 오른쪽 클릭 · 층 단추 옆 「+」(읽기만 층 = 숨김) · 빈 나무 안내글 맞춤 - 찾기 칸 하나 — 띄어쓰기 낱말 모두(AND · 대소문자 무시) · 이름 · 키 · 경로(구분 · 상세구분) · 단위 · 공용 matchText 를 나무 · 일위대가 조합 목록이 함께 씀(목록의 따로 찾기 칸 없앰) - 시험 — 찾기 AND · 새 배치 맞춤 Co-Authored-By: Claude Opus 5.5 --- .../M02_MasterTemplete_Labor.ts | 23 +++++++------- .../M02_MasterTemplete_Labor_Edit.ts | 15 ++++++---- .../M02_MasterTemplete_TreePanel.css | 22 ++++++++++++++ .../M02_MasterTemplete_TreePanel.ts | 30 ++++++++++--------- .../M02_MasterTemplete_TreePanel_Model.ts | 11 +++++-- .../M02_MasterTemplete_TreePanel_Roots.ts | 7 ++++- .../M02_MasterTemplete_UI_Page.ts | 8 +++-- .../M02_MasterTemplete_UI_Side.ts | 18 ++++++++--- resources/tester/helper_m02_treepanel.cjs | 9 ++++++ resources/tester/test_m02_combo_screen.py | 7 +++-- resources/tester/test_m02_labor_tree.py | 4 +-- .../tester/test_m02_structure_options.py | 3 +- resources/tester/test_m02_treepanel.py | 9 +++++- ui_template/ui_template_locale_m2.ts | 12 +++++--- 14 files changed, 127 insertions(+), 51 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor.ts index a4804095..cc22a464 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.ts @@ -42,6 +42,8 @@ export interface LaborLibrary { root: HTMLElement; /** 좌측 패널 나무 */ tree: HTMLElement; + /** 나무 맨 위 더하기 「+」(층 단추 옆) */ + add: HTMLElement; refresh: () => Promise; /** 메인 칸 「저장 안 한 고침」 손잡이 */ bind: (setDirty: (dirty: boolean) => void) => void; @@ -103,9 +105,16 @@ export function buildLaborLibrary(opt: LaborLibraryOptions): LaborLibrary { else editor.showList(); }, }); + // 찾기 칸 하나(PLAN 29-6) — 나무와 조합 목록을 같은 글 · 같은 규칙으로 거름 + tree.search + .querySelector("input") + ?.addEventListener("input", (event) => + editor.setQuery((event.target as HTMLInputElement).value), + ); return { root: el("div", { className: "m02-labor", children: [tree.search, editor.root] }), tree: tree.root, + add: tree.add, refresh: () => tree.refresh(), bind: (fn) => (setDirty = fn), }; @@ -120,8 +129,8 @@ export interface LaborSectionOptions { onOpen: () => void; /** 라이브러리 — `reveal` = 메인 칸에 일위대가 화면을 띄움(나무 항목을 누를 때) */ make: (reveal: () => Promise) => LaborLibrary; - /** 줄 아래 층 단추(PLAN 29-2) */ - bar?: HTMLElement; + /** 층 단추(PLAN 29-2) — `add` = 나무 맨 위 더하기 「+」를 단추 옆에 */ + bar?: (add: HTMLElement) => HTMLElement; } export interface LaborSection { @@ -132,16 +141,11 @@ export interface LaborSection { clear: () => void; } +/** 컨테이너 몸 = 층 단추 + 나무(같은 이름 줄 단추 없음 · 컨테이너 제목을 펼치면 엶 · PLAN 29-6) */ export function buildLaborSection(opt: LaborSectionOptions): LaborSection { - const row = el("button", { - className: "m02-side__item ui-sidebar-row", - text: tc("Library"), - attrs: { type: "button", "data-labor-row": "" }, - }); let opened = false; const mark = (on: boolean): void => { opened = on; - row.classList.toggle("is-active", on); }; async function open(): Promise { @@ -156,8 +160,7 @@ export function buildLaborSection(opt: LaborSectionOptions): LaborSection { void library.refresh(); const body = el("div", { className: "m02-side__items ui-sidebar-list", - children: [row, ...(opt.bar ? [opt.bar] : []), library.tree], + children: [...(opt.bar ? [opt.bar(library.add)] : []), library.tree], }); - row.addEventListener("click", () => void open()); return { title: tc("Title"), body, open, clear: () => mark(false) }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Edit.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Edit.ts index ba54c932..efc00c27 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Edit.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Edit.ts @@ -26,6 +26,7 @@ import { import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; import { buildPreview } from "./M02_MasterTemplete_Labor_Preview"; import { tc } from "./M02_MasterTemplete_Labor_Text"; +import { matchText } from "./M02_MasterTemplete_TreePanel_Model"; import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"; import "./M02_MasterTemplete_Labor.css"; @@ -77,6 +78,8 @@ export interface LaborEditor { isDirty: () => boolean; /** 열린 조합 키 — 목록이면 null · 새 조합이면 빈 글 */ current: () => string | null; + /** 라이브러리 찾기 칸 글 — 목록을 같은 규칙(`matchText`)으로 거름 */ + setQuery: (text: string) => void; } export function createLaborEditor(opt: LaborEditOptions): LaborEditor { @@ -89,11 +92,11 @@ export function createLaborEditor(opt: LaborEditOptions): LaborEditor { let current: { combo: Combo; version: string; saved: string } | null = null; let q = ""; const view = el("div", { className: "m01-logic__editor m01-combo m02-labor__edit" }); - const find = createInputField({ type: "search", placeholder: tc("Search") }); - find.input.addEventListener("input", () => { - q = find.input.value.trim().toLowerCase(); + // 찾기 칸은 라이브러리 하나(상세 위 · 나무와 같은 글 · PLAN 29-6) — `setQuery` 로 받음 + const setQuery = (text: string): void => { + q = text; if (!current) drawList(); - }); + }; const logicsReady = fetchLibLogics() .then((all) => { @@ -111,7 +114,7 @@ export function createLaborEditor(opt: LaborEditOptions): LaborEditor { nav++; current = null; opt.onDirty(false); - const shown = items.filter((x) => !q || `${x.키} ${x.이름}`.toLowerCase().includes(q)); + const shown = items.filter((x) => matchText([x.키, x.이름, x.구분, x.상세구분, x.단위], q)); const heads = [ tc("Head_Key"), tc("Head_Name"), @@ -142,7 +145,6 @@ export function createLaborEditor(opt: LaborEditOptions): LaborEditor { el("div", { className: "m01-logic__section-head", children: [ - find.root, ...(edit ? [createButton({ label: tc("New"), variant: "filled", onClick: () => startNew([]) })] : [el("span", { className: "m01-logic__muted", text: tc("ReadOnly") })]), @@ -387,5 +389,6 @@ export function createLaborEditor(opt: LaborEditOptions): LaborEditor { showList: drawList, isDirty: dirty, current: () => current?.combo.키 ?? null, + setQuery, }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css index ef5d9317..9450e2f2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css @@ -64,6 +64,28 @@ font-weight: 600; } +/* 층 단추 옆 「+」 — 나무 맨 위 폴더 · 항목 더하기(PLAN 29-6) */ +.m02-tree__add { + min-width: 1.75rem; + padding: var(--spacing-4) var(--spacing-8); + border: 1px dashed var(--color-border); + border-radius: var(--radius-buttons); + background: none; + color: var(--color-text-secondary); + font: inherit; + font-weight: 600; + cursor: pointer; +} + +.m02-tree__add:hover { + border-color: var(--color-accent); + color: var(--color-accent); +} + +.m02-tree__add[hidden] { + display: none; +} + .m02-layers__pick { display: inline-flex; align-items: center; diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 032b80b9..50d53bdb 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -47,8 +47,6 @@ export interface TreePanelOptions { layer: Layer; /** 남의 층 주인(회사 `{C}` · 개인 `{C}/{U}`) — 없으면 자기 층 */ owner?: string | null; - /** 맨 위 줄 글 — 없으면 층 이름 */ - label?: string; /** 나무 읽기 · 고치기 길 — 없으면 구조물 조합 나무(`/layers/{층}/tree`) */ api?: { fetch: (layer: Layer, owner: string | null) => Promise; @@ -73,6 +71,8 @@ export interface TreePanelHandle { root: HTMLElement; /** 찾기 칸 — 부르는 쪽이 상세 위쪽에 붙임(PLAN 29-2) */ search: HTMLElement; + /** 「+」 — 맨 위 폴더 · 항목 더하기 · 부르는 쪽이 층 단추 옆에 붙임(읽기만 층 = 숨김 · 29-6) */ + add: HTMLElement; refresh: () => Promise; /** 표에서 고른 열을 나무에 표시(null = 지움) */ select: (colId: string | null) => void; @@ -84,15 +84,6 @@ const why = (error: unknown): string => (error instanceof Error ? error.message const hint = (text: string): HTMLElement => el("p", { className: "m02-tree__hint", text }); export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { - const layerLabel = - opt.label ?? - L( - opt.layer === "company" - ? "M02_PickerCompany" - : opt.layer === "personal" - ? "M02_PickerPersonal" - : "M02_PickerSystem", - ); const owner = opt.owner ?? null; const api = opt.api ?? { fetch: fetchTree, edit: editTree }; let view: TreeView | null = null; @@ -107,6 +98,16 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { const body = el("div", { className: "m02-tree__body", attrs: { tabindex: "-1" } }); const root = el("div", { className: "m02-tree", children: [body] }); search.root.classList.add("m02-tree__search"); + // 층 단추 옆 「+」 — 맨 위에 폴더 · 항목 더하기(뿌리 줄 오른쪽 클릭을 갈음) + const add = el("button", { + className: "m02-tree__add", + text: "+", + attrs: { type: "button", title: L("M02_TreeAddTop"), "aria-label": L("M02_TreeAddTop") }, + }); + add.hidden = !opt.canEdit; + add.addEventListener("click", (event) => + openTreeMenu(event, menuOf({ kind: "folder", path: [] })), + ); /* --- 그리기 --- */ const menuOf = (target: Target): TreeMenuItem[] => { @@ -158,7 +159,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { return node( { kind: "folder", path: folder.path }, { - label: folder.path.length ? folder.path[folder.path.length - 1] : layerLabel, + label: folder.path[folder.path.length - 1] ?? "", // 맨 위(빈 경로)는 그리지 않음 count: countItems(folder), folder: true, children: kids, @@ -184,7 +185,8 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { const tree = buildFolders(view, text); const none = countItems(tree) === 0 && (searching || !tree.folders.length); body.replaceChildren( - ...renderTree([folderNode(tree, searching)], make), + // 맨 위 뿌리 줄 없음 — 폴더 · 항목부터(PLAN 29-6 · 뿌리 메뉴 = 빈 자리 오른쪽 클릭 · 「+」) + ...renderTree(folderNode(tree, searching).children ?? [], make), ...(none ? [ hint( @@ -401,5 +403,5 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { } }); - return { root, search: search.root, refresh, select }; + return { root, search: search.root, add, refresh, select }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.ts index 3164a829..30a0cbc1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.ts @@ -25,13 +25,18 @@ export const itemIdOf = (item: TreeItem): string => `i|${item.열}`; export const leafName = (item: TreeItem): string => item.경로[item.경로.length - 1] ?? item.이름; export const folderOf = (item: TreeItem): string[] => item.경로.slice(0, -1); -/** 찾기 — 경로 · 키 · 일위대가 키에 모든 낱말이 들어 있는 항목(빈 글이면 전부) */ -export function matches(item: TreeItem, text: string): boolean { +/** 찾기 규칙 하나(PLAN 29-6) — 띄어쓰기로 나눈 낱말이 모두 들어 있으면 참(AND · 대소문자 무시 · 빈 글 = 참) · + * 구조물 조합 · 일위대가 라이브러리(나무 · 조합 목록)가 함께 씀 */ +export function matchText(parts: (string | null | undefined)[], text: string): boolean { const words = text.trim().toLowerCase().split(/\s+/).filter(Boolean); - const hay = [...item.경로, item.key, item.일위대가 ?? ""].join(" ").toLowerCase(); + const hay = parts.filter(Boolean).join(" ").toLowerCase(); return words.every((word) => hay.includes(word)); } +/** 나무 항목 찾기 — 이름 · 키 · 경로(폴더 · 구분 · 상세구분) · 단위 · 일위대가 키 */ +export const matches = (item: TreeItem, text: string): boolean => + matchText([...item.경로, item.이름, item.key, item.단위, item.일위대가], text); + /** 폴더 나무 — 찾는 글이 있으면 맞는 항목과 그 위 폴더만 · 없으면 빈 폴더까지 전부 */ export function buildFolders(view: TreeView, text = ""): FolderNode { const searching = text.trim() !== ""; diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts index 679bd808..20b03214 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts @@ -28,6 +28,8 @@ export interface LayerBarOptions { company: string | null; /** 다른 층을 누름 — `owner` null = 자기 층 */ go: (root: Root, owner: string | null) => void; + /** 단추 옆 「+」(고른 층 나무 맨 위 폴더 · 항목 더하기 · PLAN 29-6) */ + add?: HTMLElement; } /** 회사 단추의 주인 — 볼 회사가 자기 회사면 null(자기 층) */ @@ -71,7 +73,10 @@ export function createLayerBar(opt: LayerBarOptions): HTMLElement { ...(opt.company ? [button("company", companyOwner(opt.company, user))] : []), ...(ownPersonal ? [button("personal", null)] : []), ]; - const row = el("div", { className: "m02-layers__row", children: buttons }); + const row = el("div", { + className: "m02-layers__row", + children: [...buttons, ...(opt.add ? [opt.add] : [])], + }); const bar = el("div", { className: "m02-layers", children: [row] }); if (isSystem && opt.company) row.append(buildPeople(opt)); // 같은 줄 · 시스템 관리자만 return bar; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 490814e4..fbd44fc6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -76,12 +76,14 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise + // 단추 옆 「+」 = 그 나무 맨 위 더하기(뿌리 줄을 갈음 · 29-6) + const layerBar = (open: OpenTarget, add: HTMLElement): HTMLElement => createLayerBar({ user, active: { root: entry.root, owner }, company, go: (next, nextOwner) => void go(next, nextOwner, open), + add, }); const main = createMain( isAdmin, @@ -114,7 +116,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise buildLaborSection({ show: main.showView, @@ -123,7 +125,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise layerBar("일위대가", add), make: (reveal) => buildLaborLibrary({ scope: { layer, owner }, canEdit: isAdmin, reveal }), }), }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index 6364127b..d60cb45a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -151,16 +151,26 @@ export function buildSide(opt: SideOptions): SideHandle { onOpen: () => (matset.clear(), labor?.clear(), paint()), }); const labor = opt.labor?.({ onOpen: () => (matset.clear(), options.clear(), paint()) }); - // 구조물 집계 양식 — 집계표 줄 아래 하위 컨테이너 넷(들여 · 같이 접힘) · 처음부터 펼침(집계표가 열려 있음) + /** 컨테이너 제목을 펼치면 그 화면을 엶 — 같은 이름 줄 단추를 갈음(PLAN 29-6) */ + const openOnExpand = (sec: HTMLElement, run: () => void): HTMLElement => { + sec.querySelector(".ui-collapsible__title")?.addEventListener("click", () => { + if (sec.classList.contains("is-collapsed")) run(); + }); + return sec; + }; + // 구조물 집계 양식 — 층 단추 · 나무 아래 하위 컨테이너 넷(들여 · 같이 접힘) · 처음부터 펼침(집계표가 열려 있음) · + // 같은 이름 줄 단추 없음(제목이 그 이름 · 29-6) const summary = section( L("M02_KindTable"), - bodies.get("table")!, - ...(opt.tree ?? []), + el("div", { className: "m02-side__items ui-sidebar-list", children: opt.tree ?? [] }), ...COLUMN_KINDS.map((k, i) => section(k.label(), columnBodies[i])), matsetSection, ); summary.classList.remove("is-collapsed"); summary.classList.add("m02-side__summary"); + openOnExpand(summary, async () => { + if (!isPicked("table", STRUCTURE_TABLE) && (await opt.confirmLeave())) opt.onOpen(null); + }); // 구조물 옵션 보이기 · 도면 양식은 시스템 층(마스터 템플릿)에만 const extras = layer === "system" @@ -172,7 +182,7 @@ export function buildSide(opt: SideOptions): SideHandle { const listHost = el("div", { className: "m02-side__lists ui-sidebar-panel", children: [ - ...(labor ? [section(labor.title, labor.body)] : []), + ...(labor ? [openOnExpand(section(labor.title, labor.body), () => void labor.open())] : []), summary, ...(extras ? [extras.옵션, extras.도면] : []), ], diff --git a/resources/tester/helper_m02_treepanel.cjs b/resources/tester/helper_m02_treepanel.cjs index 887c03dc..6381beb5 100644 --- a/resources/tester/helper_m02_treepanel.cjs +++ b/resources/tester/helper_m02_treepanel.cjs @@ -50,5 +50,14 @@ process.stdout.write( leaf: m.leafName(view.items[0]), dir: m.folderOf(view.items[0]), byKey: m.buildFolders(view, "ss000003").items.length + m.buildFolders(view, "SS000003").folders.length, + // 찾기 AND(PLAN 29-6) — 낱말 모두 · 대소문자 무시 · 순서 무관 · 빈 글 = 참 + and: [ + m.matchText(["돌기슭막이", "메쌓기", "SS000001", "m"], "메쌓기 ss000001"), + m.matchText(["돌기슭막이", "메쌓기", "SS000001", "m"], "ss000001 돌기슭"), + m.matchText(["돌기슭막이", "메쌓기", "SS000001", "m"], "메쌓기 찰쌓기"), + m.matchText(["UC3-000001", "브레인 측구", null, "배수"], ""), + m.matchText(["UC3-000001", "브레인 측구", null, "배수"], "측구 배수"), + ], + andTree: m.countItems(m.buildFolders(view, "돌기슭막이 찰쌓기")), }), ); diff --git a/resources/tester/test_m02_combo_screen.py b/resources/tester/test_m02_combo_screen.py index 4535349f..fbd2312f 100644 --- a/resources/tester/test_m02_combo_screen.py +++ b/resources/tester/test_m02_combo_screen.py @@ -91,11 +91,14 @@ def test_참조표는_열_머리를_못_고침(): def test_나무는_좌측_패널_찾기_칸은_상세_위쪽(): side = _text(M02 / "M02_MasterTemplete_UI_Side.ts") - assert "tree?: HTMLElement[];" in side and "...(opt.tree ?? [])," in side + assert "tree?: HTMLElement[];" in side and "children: opt.tree ?? []" in side assert 'info.이름 === STRUCTURE_TABLE ? L("M02_KindTable")' in side # 좌측 줄 이름 page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert "createTreePanel({" in page and "tree.select(column)" in page - assert 'tree: [layerBar("구조물조합"), tree.root],' in page and "search: tree.search," in page + assert ( + 'tree: [layerBar("구조물조합", tree.add), tree.root],' in page + and "search: tree.search," in page + ) main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") assert 'at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project"' in main assert 'isCombo(next) ? L("M02_KindTable")' in main # 상세 제목 diff --git a/resources/tester/test_m02_labor_tree.py b/resources/tester/test_m02_labor_tree.py index e1282239..c348a04b 100644 --- a/resources/tester/test_m02_labor_tree.py +++ b/resources/tester/test_m02_labor_tree.py @@ -83,8 +83,8 @@ def test_뿌리를_바꿔도_일위대가_화면_유지_해시도_맞춤(): page = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts").read_text("utf-8") assert "go: (next, nextOwner) => void go(next, nextOwner, open)," in page # 두 벌 같은 길 assert ( - 'bar: layerBar("일위대가"),' in page - and 'tree: [layerBar("구조물조합"), tree.root],' in page + 'bar: (add) => layerBar("일위대가", add),' in page + and 'tree: [layerBar("구조물조합", tree.add), tree.root],' in page ) labor = page.split("buildLaborSection({")[1].split("make:")[0] assert 'mark("일위대가")' in labor # 여는 순간 해시 open diff --git a/resources/tester/test_m02_structure_options.py b/resources/tester/test_m02_structure_options.py index df0ad925..6df8d180 100644 --- a/resources/tester/test_m02_structure_options.py +++ b/resources/tester/test_m02_structure_options.py @@ -122,7 +122,8 @@ def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에 ) # 차례 = 일위대가 조합(27-2b) → 구조물 조합 → 옵션 → 도면 양식 order = [ - "...(labor ? [section(labor.title, labor.body)] : []),", + "...(labor ? [openOnExpand(section(labor.title, labor.body), () => void labor.open())]" + " : []),", " summary,", " ...(extras ? [extras.옵션, extras.도면] : []),", ] diff --git a/resources/tester/test_m02_treepanel.py b/resources/tester/test_m02_treepanel.py index 2e1835d1..324ad23c 100644 --- a/resources/tester/test_m02_treepanel.py +++ b/resources/tester/test_m02_treepanel.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -"""M02 관리 화면 구조물 라이브러리(PLAN 26-3a) 자료 셈 회귀검증 — 폴더 나무 · 빈 폴더 · 깊이 5 · 찾기 · 폴더 목록. +"""M02 구조물 라이브러리(PLAN 26-3a) 자료 셈 회귀검증 — 폴더 나무 · 빈 폴더 · 깊이 5 · 찾기 · +폴더 목록. 항목 경로 = 폴더 이름 배열 + 마지막 마디가 항목 이름 · 찾는 글이 있으면 맞는 항목과 위 폴더만. Node 헬퍼(`helper_m02_treepanel.cjs`)가 TS 를 트랜스파일해 돌리고, 여기서 값을 판정한다. @@ -58,6 +59,12 @@ def test_키로도_찾음(results): assert results["byKey"] == 1 +def test_찾기는_낱말_모두_AND(results): + """PLAN 29-6 — 띄어쓰기로 나눈 낱말을 모두 담은 것만 · 대소문자 무시 · 빈 글 = 전부.""" + assert results["and"] == [True, True, False, True, True] + assert results["andTree"] == 1 # 「돌기슭막이 찰쌓기」 = c2 하나 + + def test_폴더_목록과_위_길(results): assert ["다섯", "둘", "셋", "넷"] in results["folders"] assert ["빈 폴더"] in results["folders"] diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 57809b43..1a2b7df3 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -104,11 +104,15 @@ export const ui_locales_m2 = { M02_RootReadOnly: ["읽기", "read-only"], M02_RootNone: ["볼 수 있는 층이 없음", "Nothing to show"], M02_PeoplePick: ["사람 고르기", "Pick a person"], - M02_TreeSearch: ["항목 찾기", "Find item"], - M02_TreeEmptyEdit: [ - "항목이 없음 — 맨 위 줄을 오른쪽 클릭해 폴더 · 항목을 더함", - "No items — right-click the top row to add a folder or item", + M02_TreeSearch: [ + "찾기 — 이름 · 키 · 폴더 · 단위 (낱말 여럿 = 모두 담은 것)", + "Find — name, key, folder, unit (all words)", ], + M02_TreeEmptyEdit: [ + "항목이 없음 — 빈 자리 오른쪽 클릭 또는 층 단추 옆 「+」 로 폴더 · 항목을 더함", + "No items — right-click an empty area or use + beside the layer buttons to add", + ], + M02_TreeAddTop: ["맨 위에 폴더 · 항목 더하기", "Add folder or item at top"], M02_TreeEmpty: ["항목이 없음", "No items"], M02_TreeNoHit: ["찾은 항목이 없음", "No matching item"], M02_TreeAddFolder: ["폴더 더하기", "Add folder"],