From ab84c763b79f65f093efcc684ab445cb5adb0b36 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:22:24 +0900 Subject: [PATCH] =?UTF-8?q?refactor(M02):=20=EC=A2=8C=EC=B8=A1=20=E3=80=8C?= =?UTF-8?q?=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=EC=A1=B0=ED=95=A9?= =?UTF-8?q?=E3=80=8D=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=C2=B7=20?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EB=82=98=EB=AC=B4=20=C2=B7=20=EB=91=98?= =?UTF-8?q?=EC=A7=B8=20=EC=B8=B5=20=EB=8B=A8=EC=B6=94=20=EB=B2=8C=20?= =?UTF-8?q?=EC=A7=80=EC=9B=80(39-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Labor.ts · Labor_Tree.ts 지움 · 페이지 · 좌측 이음 · 해시 표시(mark) 지움 - 나무 부품 일위대가 전용 칸(clipKind · maxDepth · item) 지움 - 옛 나무 시험을 새 짜임 검사로 갈음 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- .../M02_MasterTemplete_Labor.ts | 222 ------------------ .../M02_MasterTemplete_Labor_Tree.ts | 45 ---- .../M02_MasterTemplete_TreePanel.ts | 23 +- .../M02_MasterTemplete_UI_Page.ts | 36 +-- .../M02_MasterTemplete_UI_Side.ts | 24 +- .../M02_MasterTemplete_UI_Style.css | 8 - resources/tester/test_m02_labor_tree.py | 131 ++--------- resources/tester/test_m02_layer_screen.py | 13 +- 8 files changed, 39 insertions(+), 463 deletions(-) delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Labor.ts delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor.ts deleted file mode 100644 index 985d1a1f8..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.ts +++ /dev/null @@ -1,222 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Labor.ts - * 좌측 「일위대가 조합」 컨테이너(구조물 조합 위 · PLAN 27-2b) — 「일위대가 라이브러리」 줄 아래 층 단추 - * (`TreePanel_Roots`) · 조합 나무(폴더 [구분, 상세구분] 깊이 2 · 구조물 라이브러리와 같은 `TreePanel` · - * 좌측 패널 안 · PLAN 29-2) · 줄 · 나무 항목을 누르면 메인 칸에 찾기 칸 + 조합 목록 · 상세(`Labor_Edit`). - * 나무 고침 = `POST /api/m02/layers/{층}/combos/tree` · 「조합 더하기」 = 그 폴더를 채운 빈 조합(저장해야 생김). - * ========================================================================== */ - -import { el, showConfirmDialog } from "@ui/ui_template_elements"; -import { ApiError } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; -import { StaleError } from "./M02_MasterTemplete_Api_Fetch"; -import { - editComboTree, - editComboTreeMany, - fetchComboList, - pasteCombos, - type ComboList, - type ComboTreeEdit, - type LaborScope, -} from "./M02_MasterTemplete_Labor_Api"; -import { createLaborEditor } from "./M02_MasterTemplete_Labor_Edit"; -import { noteOf, toView } from "./M02_MasterTemplete_Labor_Tree"; -import { tc } from "./M02_MasterTemplete_Labor_Text"; -import { createTreePanel } from "./M02_MasterTemplete_TreePanel"; - -/** 나무 부품이 아는 막힘으로 — 판 다름 = `StaleError("HTTP 409")` · 나머지는 서버 까닭 글 */ -function treeError(error: unknown): Error { - if (!(error instanceof ApiError)) - return error instanceof Error ? error : new Error(String(error)); - const d = error.detail as { errors?: string[]; stale?: unknown } | string | null; - const text = typeof d === "string" ? d : d?.errors?.join(" · ") || `HTTP ${String(error.status)}`; - return error.status === 409 ? new StaleError(text) : new Error(text); -} - -export interface LaborLibraryOptions { - scope: LaborScope; - canEdit: boolean; - /** 「〇〇로 복사」 — 이 사람이 쓸 수 있는 층(지금 보는 층 뺌 · 페이지가 구조물 나무와 같은 목록을 줌) */ - copyTo?: { layer: LaborScope["layer"]; owner: string | null; label: string }[]; - /** 나무 항목을 누름 — 메인 칸에 일위대가 화면을 먼저 띄움 */ - reveal: () => Promise; -} - -export interface LaborLibrary { - /** 메인 칸 — 찾기 칸 + 조합 목록 · 상세 */ - root: HTMLElement; - /** 좌측 패널 나무 */ - tree: HTMLElement; - /** 나무 맨 위 더하기 「+」(층 단추 옆) */ - add: HTMLElement; - /** 켜진 층 단추를 다시 누름 — 조합 목록으로(저장 안 한 고침은 물음 · PLAN 30-4) */ - back: () => Promise; - refresh: () => Promise; - /** 제목 줄 오른쪽 끝 도구 — 「새 조합」(읽기 층 = null · PLAN 30-5) */ - tools: HTMLElement | null; - /** 메인 칸 「저장 안 한 고침」 손잡이 */ - bind: (setDirty: (dirty: boolean) => void) => void; -} - -export function buildLaborLibrary(opt: LaborLibraryOptions): LaborLibrary { - const { scope } = opt; - let setDirty: (dirty: boolean) => void = () => {}; - let keys = new Set(); // 지금 목록의 조합 키 — 열린 조합이 지워졌나 - const took = (list: ComboList): ComboList => { - keys = new Set(list.줄.map((c) => c.키)); - editor.setList(list.줄); - return list; - }; - const editor = createLaborEditor({ - scope, - canEdit: opt.canEdit, - onDirty: (dirty) => setDirty(dirty), - onSaved: async (key) => { - await tree.refresh(); - tree.select(key); - }, - }); - const tree = createTreePanel({ - layer: scope.layer, - owner: scope.owner, - canEdit: opt.canEdit, - maxDepth: 2, - item: { add: tc("AddCombo"), name: tc("ComboName"), onAdd: (path) => editor.startNew(path) }, - api: { - fetch: async () => { - return toView(took(await fetchComboList(scope))); - }, - edit: async (_layer, edit, 판) => { - const body = { ...edit, 판 } as ComboTreeEdit; - const got = await editComboTree(body, scope).catch((error: unknown) => { - throw treeError(error); - }); - return toView(took(got), noteOf(edit, got.지운조합)); - }, - // 여러 개 옮기기 · 지우기(PLAN 30-3) — 고른 것마다 나무 고침 하나 · 서버가 한 번에 저장 - many: async (_layer, body, 판) => { - const ops = body.대상.map((ref) => ({ - op: body.op, - ...(ref.키 ? { 키: ref.키 } : { 경로: ref.경로 }), - ...(body.op === "move" ? { 새경로: body.새경로 ?? [] } : {}), - })); - const got = await editComboTreeMany(ops, 판, scope).catch((error: unknown) => { - throw treeError(error); - }); - return toView(took(got), { op: body.op, 지운항목: got.지운조합 }); - }, - // 붙여넣기 · 「〇〇로 복사」 — 대상 층(`layer` · `owner`)에 새 키 · 이 나무가 아니면 이 나무는 그대로 - paste: async (layer, owner, clip, dest) => { - const to = { layer, owner } as LaborScope; - const pick = { - 키: clip.refs.flatMap((ref) => (ref.키 ? [ref.키] : [])), - 폴더: clip.refs.flatMap((ref) => (ref.키 ? [] : [ref.경로])), - }; - const got = await fetchComboList(to) - .then((list) => pasteCombos(clip.from as LaborScope, pick, dest, list.판, to)) - .catch((error: unknown) => { - throw treeError(error); - }); - const here = layer === scope.layer && (owner ?? null) === (scope.owner ?? null); - return toView(took(here ? got : await fetchComboList(scope))); - }, - }, - clipKind: "일위대가", - copyTo: opt.copyTo, - onPick: async (item) => { - await opt.reveal(); - tree.select(item.key); - void editor.open(item.key); - }, - // 나무를 고치기 전 — 저장 안 한 조합 고침은 버릴지 물음(버리면 목록으로) - settle: async () => { - if (!editor.isDirty()) return "clean"; - if (!(await showConfirmDialog(tc("BackAsk")))) return false; - editor.showList(); - return "changed"; - }, - // 나무가 바뀜 — 열린 조합은 새 판으로 다시 엶(이름 · 폴더가 바뀌었을 수 있음) · 지워졌으면 목록 - onChanged: async () => { - const open = editor.current(); - if (!open) return; - if (keys.has(open)) await editor.open(open); - 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, - back: async () => { - await opt.reveal(); // 일위대가 화면이 아니면 먼저 띄움 - if (editor.current() === null) return; - if (editor.isDirty() && !(await showConfirmDialog(tc("BackAsk")))) return; - editor.showList(); - }, - refresh: async () => { - const stop = editor.loading(); // 조합 목록 칸에도 「불러오는 중」(나무 칸은 나무 부품이 · PLAN 30-6) - try { - await tree.refresh(); - } finally { - stop(); - } - }, - tools: editor.newButton, - bind: (fn) => (setDirty = fn), - }; -} - -export interface LaborSectionOptions { - /** 메인 칸에 화면을 띄움 — 고침 표시 손잡이를 돌려받음 */ - show: (label: string, node: HTMLElement, tools?: HTMLElement | null) => (dirty: boolean) => void; - /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ - confirmLeave: () => Promise; - /** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */ - onOpen: () => void; - /** 라이브러리 — `reveal` = 메인 칸에 일위대가 화면을 띄움(나무 항목을 누를 때) */ - make: (reveal: () => Promise) => LaborLibrary; - /** 층 단추(PLAN 29-2) — `add` = 나무 맨 위 더하기 「+」를 단추 옆에 */ - bar?: (add: HTMLElement, back: () => void) => HTMLElement; - /** 나무를 감쌈 — 남의 회사 「개인」이면 사람 나무 안에(PLAN 29-6) */ - wrap?: (tree: HTMLElement) => HTMLElement; -} - -export interface LaborSection { - title: string; - body: HTMLElement; - open: () => Promise; - /** 다른 화면을 열었음 — 고름 표시만 지움 */ - clear: () => void; -} - -/** 컨테이너 몸 = 층 단추 + 나무(같은 이름 줄 단추 없음 · 컨테이너 제목을 펼치면 엶 · PLAN 29-6) */ -export function buildLaborSection(opt: LaborSectionOptions): LaborSection { - let opened = false; - const mark = (on: boolean): void => { - opened = on; - }; - - async function open(): Promise { - if (opened || !(await opt.confirmLeave())) return; - library.bind(opt.show(tc("Library"), library.root, library.tools)); - opt.onOpen(); - mark(true); - await library.refresh(); - } - // 나무는 좌측 패널에 늘 있음(PLAN 29-2) — 처음부터 만들고 읽음 - const library = opt.make(open); - void library.refresh(); - const body = el("div", { - className: "m02-side__items ui-sidebar-list", - children: [ - ...(opt.bar ? [opt.bar(library.add, () => void library.back())] : []), - opt.wrap ? opt.wrap(library.tree) : library.tree, - ], - }); - return { title: tc("Title"), body, open, clear: () => mark(false) }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts deleted file mode 100644 index d18017cae..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Labor_Tree.ts - * 일위대가 라이브러리 나무 자료(화면 없음) — 조합 목록 → 나무 부품(`TreePanel`) 모양 · 나무 고침 뒤 알림. - * 조합 폴더 = [구분] · [구분, 상세구분](깊이 2 · PLAN 27-2a) · 항목 = 조합(열 = 키). - * ========================================================================== */ - -import type { TreeEdit, TreeNote, TreeView } from "./M02_MasterTemplete_Api_Fetch"; -import type { ComboList, ComboSummary } from "./M02_MasterTemplete_Labor_Api"; - -/** 조합의 폴더 — [구분] · [구분, 상세구분] · 구분 없으면 맨 위 */ -export const pathOf = (c: ComboSummary): string[] => - c.구분 ? (c.상세구분 ? [c.구분, c.상세구분] : [c.구분]) : []; - -/** 조합 목록 → 나무 부품 모양(항목 = 조합 · 열 = 키) */ -export const toView = (list: ComboList, note?: TreeNote): TreeView => ({ - 판: list.판, - 폴더: list.폴더, - items: list.줄.map((c) => ({ - key: c.키, - 열: c.키, - 경로: [...pathOf(c), c.이름], - 이름: c.이름, - 단위: c.단위, - })), - ...(note ? { 알림: note } : {}), -}); - -/** 나무 고침 뒤 알림 — 새 · 바뀐 폴더를 펼치고 고친 조합은 고른 채 */ -export function noteOf(edit: TreeEdit, gone: string[]): TreeNote { - const at = edit.경로 ?? []; - const last = at[at.length - 1] ?? ""; - if (edit.키 && edit.op !== "delete") { - const where = edit.op === "move" ? (edit.새경로 ?? []) : at; - return { op: edit.op, 열: edit.키, 경로: [...where, edit.이름 ?? ""], 지운항목: gone }; - } - const path = - edit.op === "add-folder" - ? [...at, edit.이름 ?? ""] - : edit.op === "rename" - ? [...at.slice(0, -1), edit.이름 ?? ""] - : edit.op === "move" - ? [...(edit.새경로 ?? []), last] - : undefined; - return { op: edit.op, ...(path ? { 경로: path } : {}), 지운항목: gone }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 426b52b8d..014e9c9b1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -81,18 +81,12 @@ export interface TreePanelOptions { dest: string[], ) => Promise; }; - /** 복사해 둔 것의 나무 종류 — 같은 종류 나무끼리만 붙임(기본 「구조물」) */ - clipKind?: string; /** 항목 추가 창의 일위대가 고르기(PLAN 31-2 · 세 층 조합 · 찾기 AND) — 없으면 그 줄 없음 */ pickCombo?: () => Promise<{ key: string; label: string } | null>; /** 「〇〇로 복사」 — 고른 것을 그 층 맨 위에 바로(층을 안 바꿈 · `label` 의 {n} = 개수 · `api.paste` 로) */ copyTo?: { layer: Layer; owner: string | null; label: string }[]; /** 이 층을 고칠 수 있나 — 아니면 메뉴 · 키 없음 */ canEdit: boolean; - /** 폴더 깊이 한도 — 닿은 폴더엔 「폴더 더하기」 없음(일위대가 라이브러리 = 2) */ - maxDepth?: number; - /** 항목 부르는 말 · 「항목 더하기」 대신 부르는 쪽이 새 항목 화면을 엶(일위대가 조합) */ - item?: { add: string; name: string; onAdd?: (path: string[]) => void }; /** 항목을 누름 — 표에서 그 열을 고름 */ onPick: (item: TreeItem) => void; /** 고치기 전에 저장 안 한 표 고침을 처리 — 취소면 false · 저장했으면 "changed" */ @@ -123,7 +117,7 @@ const hint = (text: string): HTMLElement => el("p", { className: "m02-tree__hint export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { const owner = opt.owner ?? null; const api = opt.api ?? { fetch: fetchTree, edit: editTree, many: editTreeMany, paste: pasteTree }; - const clipKind = opt.clipKind ?? "구조물"; + const clipKind = "구조물"; // 복사해 둔 것의 나무 종류 — 같은 종류 나무끼리만 붙임 let view: TreeView | null = null; let text = ""; let activeCol: string | null = null; @@ -213,13 +207,8 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { return [ ...(target.kind === "folder" ? [ - ...(opt.maxDepth !== undefined && target.path.length >= opt.maxDepth - ? [] - : [{ label: L("M02_TreeAddFolder"), run: () => void addFolder(target.path) }]), - { - label: opt.item?.add ?? L("M02_TreeAddItem"), - run: () => void addItem(target.path), - }, + { label: L("M02_TreeAddFolder"), run: () => void addFolder(target.path) }, + { label: L("M02_TreeAddItem"), run: () => void addItem(target.path) }, ] : []), ...(isTop @@ -466,7 +455,6 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { async function addItem(parent: string[]): Promise { if (!(await ready())) return; - if (opt.item?.onAdd) return opt.item.onAdd(parent); // 종류(맨 위 폴더 · 고른 폴더면 그 안) + 이름 + 일위대가 · 단위 안 받음(PLAN 31-2) const kinds = view ? buildFolders(view).folders.map((f) => f.path[0]) : []; const got = await askNewItem({ @@ -490,10 +478,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { const got = await askText(L("M02_TreeRename"), [ { key: "name", - label: - target.kind === "item" - ? (opt.item?.name ?? L("M02_TreeItemName")) - : L("M02_TreeFolderName"), + label: target.kind === "item" ? L("M02_TreeItemName") : L("M02_TreeFolderName"), value: nameOf(target), }, ]); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 2be94a20d..a19bc482a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -27,7 +27,6 @@ import { type OpenTarget, type Root, } from "./M02_MasterTemplete_Layer"; -import { buildLaborLibrary, buildLaborSection } from "./M02_MasterTemplete_Labor"; import { headRenameEdit, pickCombo, @@ -86,14 +85,6 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise main.settle(), onChanged: (colId) => main.openTable(layer, colId, true), }); - // 일위대가 화면이 떠 있나 — 층을 바꿔도 일위대가로 다시 엶 · - // 해시 `open` 도 맞춤(새로 고침 · 카드 진입과 같게) · 옵션 · 도면은 옛 규칙(구조물 조합으로) - let view: OpenTarget = entry.open; - const mark = (open: OpenTarget): void => { - view = open; - const query = new URLSearchParams(entryQuery({ ...entry, open })); - history.replaceState(history.state, "", `#/${ROUTES.M02_MASTER_TEMPLATE}?${query}`); - }; const go = async (next: Root, nextOwner: string | null, open: OpenTarget): Promise => { // 같은 주소 안 이동은 머리 가드가 안 물음 — 저장 안 한 고침은 여기서 if (!(await main.confirmLeave())) return; @@ -102,9 +93,9 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void): HTMLElement => createLayerBar({ user, @@ -127,11 +118,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void Promise.all([side.refresh(), tree.refresh()]), - () => { - side.paint(); - // 일위대가 화면에서 양식으로 옮김(일위대가 · 옵션 화면은 current 없음) - if (main.current() && view === "일위대가") mark("구조물조합"); - }, + () => side.paint(), { top: readOnlyBar?.root, search: tree.search, @@ -168,19 +155,6 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void main.openTable(layer)), peopleTree("구조물조합", tree.root), ], - labor: ({ onOpen }) => - buildLaborSection({ - show: main.showView, - confirmLeave: main.confirmLeave, - onOpen: () => { - onOpen(); - mark("일위대가"); - }, - bar: (add, back) => layerBar("일위대가", add, back), - wrap: (panel) => peopleTree("일위대가", panel), - make: (reveal) => - buildLaborLibrary({ scope: { layer, owner }, canEdit: isAdmin, reveal, copyTo }), - }), }); const layout = el("div", { className: "ui-workflow-layout m02-master" }); @@ -200,8 +174,8 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise Promise; /** 메인 칸에 양식 밖 화면(자재 조합 상세)을 띄움 */ showView: (label: string, node: HTMLElement) => (dirty: boolean) => void; - /** 「일위대가 조합」 컨테이너(구조물 조합 위 · PLAN 27-2b) — 페이지가 만듦(뿌리 셋이 페이지 것) */ - labor?: (hooks: { onOpen: () => void }) => LaborSection; /** 「구조물 조합」 줄 아래 — 층 단추 · 고른 층의 구조물 라이브러리 나무(PLAN 29-2 · 페이지가 만듦) */ tree?: HTMLElement[]; } @@ -85,7 +82,7 @@ export interface SideHandle { refresh: () => Promise; /** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */ paint: () => void; - /** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침(옵션은 열기까지) · 시스템 층에만 있음 */ + /** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침(옵션은 열기까지) · 옵션 · 도면은 시스템 층에만 */ reveal: (target: "옵션" | "도면" | "일위대가") => void; } @@ -136,7 +133,7 @@ export function buildSide(opt: SideOptions): SideHandle { confirmLeave: opt.confirmLeave, show: opt.showView, close: () => opt.onOpen(null), - onOpen: () => (options.clear(), labor?.clear(), paint()), + onOpen: () => (options.clear(), paint()), }); const matsetSection = section(matset.title, matset.body, ...matset.actions); // 펼칠 때마다 새로 받음 — 재료산출 옆 판이 갈라 낸 조합도 바로 보이게 @@ -148,9 +145,8 @@ export function buildSide(opt: SideOptions): SideHandle { isAdmin: canEdit(layer, opt.isAdmin), confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (matset.clear(), labor?.clear(), paint()), + onOpen: () => (matset.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", () => { @@ -181,15 +177,9 @@ export function buildSide(opt: SideOptions): SideHandle { : null; const listHost = el("div", { className: "m02-side__lists ui-sidebar-panel", - children: [ - ...(labor ? [openOnExpand(section(labor.title, labor.body), () => void labor.open())] : []), - summary, - ...(extras ? [extras.옵션, extras.도면] : []), - ], + children: [summary, ...(extras ? [extras.옵션, extras.도면] : [])], }); attachCollapsible(listHost); - // 들어오면 「일위대가 조합」 · 「구조물 조합」 둘 다 펼친 채(PLAN 31-5 · 구조물 조합은 위에서 펼침) - labor?.body.closest("section")?.classList.remove("is-collapsed"); const root = el("div", { className: "m02-side ui-sidebar-panel", children: [listHost] }); @@ -263,7 +253,6 @@ export function buildSide(opt: SideOptions): SideHandle { paintButtons(); if (opt.getOpen()) matset.clear(); if (opt.getOpen()) options.clear(); - if (opt.getOpen()) labor?.clear(); } // 여는 것은 편집 칸(`onOpen`) — 편집 칸이 열 때마다 `paint` 를 부름(페이지가 이음) @@ -388,10 +377,7 @@ export function buildSide(opt: SideOptions): SideHandle { } const reveal = (target: "옵션" | "도면" | "일위대가"): void => { - if (target === "일위대가") { - labor?.body.closest("section")?.classList.remove("is-collapsed"); - return void labor?.open(); - } + if (target === "일위대가") return; // 구조물 조합은 처음부터 펼침 extras?.[target].classList.remove("is-collapsed"); if (target === "옵션") options.body.querySelector("button")?.click(); }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 0e323ca8c..c4c51d1ca 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -222,14 +222,6 @@ font-size: 11px; } -/* 나무는 좌측 패널(PLAN 29-2) — 일위대가 화면 = 찾기 칸 위 · 조합 목록 · 상세 아래 */ -.m02-labor { - display: flex; - flex-direction: column; - gap: var(--spacing-8); - min-width: 0; -} - .m02-main__head + .m02-tree__search { margin-bottom: var(--spacing-8); } diff --git a/resources/tester/test_m02_labor_tree.py b/resources/tester/test_m02_labor_tree.py index 9268309c8..755dac40b 100644 --- a/resources/tester/test_m02_labor_tree.py +++ b/resources/tester/test_m02_labor_tree.py @@ -1,124 +1,35 @@ -"""일위대가 라이브러리 나무 자료(PLAN 27-2b · `Labor_Tree.ts`) — Node 로 TS 를 돌려 판정. +"""좌측 패널 일위대가(PLAN 39-2) — 맨 위 「일위대가 조합」 컨테이너 · 조합 나무 · 둘째 층 단추 벌 지움 · +일위대가는 구조물 조합 하위(구조물 도면 → 일위대가 → 면적산출). 글 검사(화면은 ORCA). -못박는 것 - - 조합 → 나무 항목: 경로 = [구분, 상세구분, 이름](빈 칸 건너뜀) · 열 = 키. - - 나무 고침 알림: 새 · 바뀐 · 옮긴 폴더 경로 · 조합 고침은 그 조합(열)을 고른 채 · - 지우기는 고름 없음. +옛 일위대가 라이브러리 나무 시험(PLAN 27-2b · 30-3 · 30-5)은 나무를 지우며 이 검사로 갈음. """ -import json -import shutil -import subprocess from pathlib import Path -import pytest - ROOT = Path(__file__).resolve().parents[2] -SCRIPT = r""" -const fs = require("fs"); -const ts = require(process.argv[1] + "/config/node_modules/typescript"); -const file = "/M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts"; -const src = fs.readFileSync(process.argv[1] + file, "utf8"); -const js = ts.transpileModule(src, { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, -}).outputText; -const box = { exports: {} }; -new Function("exports", "module", "require", js)(box.exports, box, require); -const m = box.exports; -const row = (키, 이름, 구분, 상세구분) => ({ 키, 이름, 구분, 상세구분, 단위: "m" }); -const list = { - 판: "v1", - 폴더: [["석축"], ["석축", "찰쌓기"], ["빈"]], - 줄: [row("UP1-000001", "가", "석축", "찰쌓기"), row("UP1-000002", "나", "석축", null), - row("UP1-000003", "다", null, "무시")], -}; -const view = m.toView(list); -const notes = [ - { op: "add-folder", 경로: ["석축"], 이름: "메쌓기" }, - { op: "rename", 경로: ["석축", "찰쌓기"], 이름: "찰" }, - { op: "move", 경로: ["석축", "찰쌓기"], 새경로: [] }, - { op: "move", 경로: ["석축"], 키: "UP1-000002", 새경로: ["빈"] }, - { op: "rename", 경로: ["석축"], 키: "UP1-000002", 이름: "나나" }, - { op: "delete", 경로: ["석축"], 키: "UP1-000002" }, - { op: "delete", 경로: ["빈"] }, -].map((e) => m.noteOf(e, e.op === "delete" ? ["UP1-000002"] : [])); -console.log(JSON.stringify({ view, notes })); -""" +M02 = ROOT / "M02_MasterTemplete" -@pytest.fixture(scope="module") -def got(): - if shutil.which("node") is None: - pytest.skip("node 가 없음") - proc = subprocess.run( - ["node", "-e", SCRIPT, str(ROOT)], capture_output=True, text=True, encoding="utf-8" - ) - assert proc.returncode == 0, proc.stderr - return json.loads(proc.stdout) +def _text(name: str) -> str: + return (M02 / name).read_text("utf-8") -def test_조합이_나무_항목으로(got): - items = got["view"]["items"] - assert [i["경로"] for i in items] == [["석축", "찰쌓기", "가"], ["석축", "나"], ["다"]] - assert [i["열"] for i in items] == ["UP1-000001", "UP1-000002", "UP1-000003"] - assert got["view"]["폴더"] == [["석축"], ["석축", "찰쌓기"], ["빈"]] - assert "알림" not in got["view"] +def test_일위대가_나무_파일과_이음이_없음(): + assert not (M02 / "M02_MasterTemplete_Labor.ts").exists() + assert not (M02 / "M02_MasterTemplete_Labor_Tree.ts").exists() + page = _text("M02_MasterTemplete_UI_Page.ts") + assert "buildLaborSection" not in page and 'layerBar("일위대가"' not in page + side = _text("M02_MasterTemplete_UI_Side.ts") + assert "LaborSection" not in side and "labor?." not in side -def test_나무_고침_알림(got): - add, rename, move, move_item, rename_item, delete_item, delete_folder = got["notes"] - assert add["경로"] == ["석축", "메쌓기"] and "열" not in add - assert rename["경로"] == ["석축", "찰"] - assert move["경로"] == ["찰쌓기"] - assert move_item["열"] == "UP1-000002" and move_item["경로"][:-1] == ["빈"] - assert rename_item["경로"] == ["석축", "나나"] - assert "열" not in delete_item and delete_item["지운항목"] == ["UP1-000002"] - assert "경로" not in delete_folder +def test_나무_부품에_일위대가_전용_칸_없음(): + panel = _text("M02_MasterTemplete_TreePanel.ts") + for gone in ("maxDepth", "opt.item", "opt.clipKind", "clipKind?:"): + assert gone not in panel, gone -def test_뿌리를_바꿔도_일위대가_화면_유지_해시도_맞춤(): - """브레인 검증 흠(27-2b) · 29-2 — 층 단추 두 벌(일위대가 라이브러리 · 구조물 조합 줄 아래) · - 일위대가 단추로 층을 바꾸면 일위대가 화면 그대로 · 좌측 「일위대가 라이브러리」를 열면 해시 - `open=일위대가`(다시 그리지 않고 주소만).""" - 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: (add, back) => layerBar("일위대가", add, back),' in page - and 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page - ) - labor = page.split("buildLaborSection({")[1].split("make:")[0] - assert 'mark("일위대가")' in labor # 여는 순간 해시 open - assert "history.replaceState" in page # hashchange 없이(화면 다시 안 그림) - assert 'if (main.current() && view === "일위대가") mark("구조물조합")' in page - - -def test_일위대가_나무에_여러_개_복사_붙여넣기_연결(): - """PLAN 30-3 — 공용 나무 부품(30-2) 콜백을 일위대가 서버 길에 이음 · - 붙임 종류는 따로(구조물과 안 섞임).""" - labor = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor.ts").read_text("utf-8") - assert 'clipKind: "일위대가"' in labor and "copyTo: opt.copyTo" in labor - assert "editComboTreeMany(ops, 판, scope)" in labor # 여러 개 옮기기 · 지우기 - assert "pasteCombos(clip.from as LaborScope, pick, dest, list.판, to)" in labor # 대상 층 - page = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts").read_text("utf-8") - assert "canEdit: isAdmin, reveal, copyTo })" in page # 구조물 나무와 같은 「〇〇로 복사」 목록 - - -def test_새_조합_단추는_제목_줄_오른쪽_목록은_공용_표(): - """PLAN 30-5 — 「새 조합」 = 제목 줄 오른쪽 끝(읽기 층 없음) · 목록 = 공용 표(줄 선). - 머리 줄도 공용 표 모양.""" - edit = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Edit.ts").read_text("utf-8") - assert ( - "newButton: edit" in edit and 'className: "ui-general-block__table m02-labor__list"' in edit - ) - assert ( - 'tc("New"), variant: "filled", onClick: () => startNew([]) })]' not in edit - ) # 목록 안 옛 자리 - labor = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor.ts").read_text("utf-8") - assert 'library.bind(opt.show(tc("Library"), library.root, library.tools))' in labor - main = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts").read_text("utf-8") - assert ( - 'slot.replaceChildren(el("span", { className: "m02-main__tools", children: [tools] }))' - in main - ) - css = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Style.css").read_text("utf-8") - assert ".m02-main__tools {\n display: inline-flex;\n margin-left: auto;" in css +def test_층_단추는_구조물_조합_한_벌(): + page = _text("M02_MasterTemplete_UI_Page.ts") + assert page.count('layerBar("') == 1 # 구조물 조합 줄 아래 하나 + assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index c7bb3f17f..299ec5e4b 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -136,7 +136,7 @@ def test_층_단추_셋_남의_회사_사람_나무(): assert 'const people = entry.root === "personal" && seesOthers(company, user);' in page assert '(await fetchOwners("personal", company).catch(() => []))[0]?.owner' in page assert 'peopleTree("구조물조합", tree.root)' in page - assert 'wrap: (panel) => peopleTree("일위대가", panel)' in page + assert 'peopleTree("일위대가"' not in page # 일위대가 사람 나무 벌은 39-2 에서 지움 panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts") assert ( "api.fetch(opt.layer, owner)" in panel and "api.edit(opt.layer, body_, 판, owner)" in panel @@ -219,17 +219,13 @@ def test_읽기만_표는_단추_고치기_모두_막음(): def test_켜진_층_단추는_목록으로_복사_끌기(): - """30-4 — 켜진 층 단추를 다시 누르면 그 층 목록(구조물 = 집계표 · 일위대가 = 조합 목록 · - 저장 안 한 고침은 물음) · 30-2 「〇〇로 복사」 = 쓸 수 있는 자기 층(지금 층 뺌) · - 끌어다 옮기기는 쓸 수 있는 층만.""" + """30-4 — 켜진 층 단추를 다시 누르면 그 층 목록(구조물 = 집계표 · 일위대가 단추 벌은 39-2 에서 지움) · + 30-2 「〇〇로 복사」 = 쓸 수 있는 자기 층(지금 층 뺌) · 끌어다 옮기기는 쓸 수 있는 층만.""" roots = _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts") assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer))' in page - assert 'bar: (add, back) => layerBar("일위대가", add, back)' in page assert "if (!accessFor(to, null, user).canWrite || (to === layer && !owner)) return [];" in page - labor = _text(M02 / "M02_MasterTemplete_Labor.ts") - assert 'if (editor.isDirty() && !(await showConfirmDialog(tc("BackAsk")))) return;' in labor panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts") assert "if (opt.canEdit) drag.arm(button, one.id);" in panel drag = _text(M02 / "M02_MasterTemplete_TreePanel_Drag.ts") @@ -259,10 +255,9 @@ def test_구조물_조합_제목_줄_새_항목(): def test_들어오면_두_컨테이너_펼침_끌기_뒤_강조_지움(): - """31-5 — 좌측 「일위대가 조합」 · 「구조물 조합」 둘 다 펼친 채 · 끌어 놓은 뒤 강조(나무 칸 + """31-5 — 좌측 「구조물 조합」 펼친 채(일위대가는 그 하위 · 39-2) · 끌어 놓은 뒤 강조(나무 칸 자신 = 맨 위 자리 포함)를 모두 지움 · 칸 밖으로 나가면 지움(칸 안 자식 위로는 그대로).""" side = _text(M02 / "M02_MasterTemplete_UI_Side.ts") - assert 'labor?.body.closest("section")?.classList.remove("is-collapsed");' in side assert 'summary.classList.remove("is-collapsed");' in side drag = _text(M02 / "M02_MasterTemplete_TreePanel_Drag.ts") mark = drag.split("const dropMark = ")[1].split("};")[0]