Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor.ts
T
eomsangdonandClaude Opus 5.5 c1ad11e869 feat(M02): 좌측 같은 이름 단추 · 나무 뿌리 줄 없앰 · 「+」 더하기 · 찾기 하나(AND) (PLAN 29-6)
- 「일위대가 조합」 · 「구조물 조합」 컨테이너 안 같은 이름 줄 단추 없앰 — 제목을 펼치면 그 화면을 엶
- 나무 맨 위 뿌리 줄 없앰 — 폴더 · 항목부터 · 맨 위 더하기는 빈 자리 오른쪽 클릭 · 층 단추 옆 「+」(읽기만 층 = 숨김) · 빈 나무 안내글 맞춤
- 찾기 칸 하나 — 띄어쓰기 낱말 모두(AND · 대소문자 무시) · 이름 · 키 · 경로(구분 · 상세구분) · 단위 · 공용 matchText 를 나무 · 일위대가 조합 목록이 함께 씀(목록의 따로 찾기 칸 없앰)
- 시험 — 찾기 AND · 새 배치 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:15:43 +09:00

167 lines
6.6 KiB
TypeScript

/* =============================================================================
* 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,
fetchComboList,
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;
/** 나무 항목을 누름 — 메인 칸에 일위대가 화면을 먼저 띄움 */
reveal: () => Promise<void>;
}
export interface LaborLibrary {
/** 메인 칸 — 찾기 칸 + 조합 목록 · 상세 */
root: HTMLElement;
/** 좌측 패널 나무 */
tree: HTMLElement;
/** 나무 맨 위 더하기 「+」(층 단추 옆) */
add: HTMLElement;
refresh: () => Promise<void>;
/** 메인 칸 「저장 안 한 고침」 손잡이 */
bind: (setDirty: (dirty: boolean) => void) => void;
}
export function buildLaborLibrary(opt: LaborLibraryOptions): LaborLibrary {
const { scope } = opt;
let setDirty: (dirty: boolean) => void = () => {};
let keys = new Set<string>(); // 지금 목록의 조합 키 — 열린 조합이 지워졌나
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.지운조합));
},
},
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,
refresh: () => tree.refresh(),
bind: (fn) => (setDirty = fn),
};
}
export interface LaborSectionOptions {
/** 메인 칸에 화면을 띄움 — 고침 표시 손잡이를 돌려받음 */
show: (label: string, node: HTMLElement) => (dirty: boolean) => void;
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
confirmLeave: () => Promise<boolean>;
/** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */
onOpen: () => void;
/** 라이브러리 — `reveal` = 메인 칸에 일위대가 화면을 띄움(나무 항목을 누를 때) */
make: (reveal: () => Promise<void>) => LaborLibrary;
/** 층 단추(PLAN 29-2) — `add` = 나무 맨 위 더하기 「+」를 단추 옆에 */
bar?: (add: HTMLElement) => HTMLElement;
}
export interface LaborSection {
title: string;
body: HTMLElement;
open: () => Promise<void>;
/** 다른 화면을 열었음 — 고름 표시만 지움 */
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<void> {
if (opened || !(await opt.confirmLeave())) return;
library.bind(opt.show(tc("Library"), library.root));
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)] : []), library.tree],
});
return { title: tc("Title"), body, open, clear: () => mark(false) };
}