Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
160 lines
6.0 KiB
TypeScript
160 lines
6.0 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Labor.ts
|
|
* 좌측 「일위대가 조합」 컨테이너(구조물 조합 위 · PLAN 27-2b) — 누르면 메인 칸에 일위대가 라이브러리:
|
|
* 왼쪽 뿌리 셋(마스터 · 회사 · 개인 · `TreePanel_Roots`) 아래 조합 나무(폴더 [구분, 상세구분] 깊이 2 ·
|
|
* 구조물 라이브러리와 같은 `TreePanel`) · 오른쪽 조합 목록 · 상세(`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;
|
|
/** 뿌리 셋 — 고른 뿌리 아래에 이 나무(`panel`)를 붙인 것 */
|
|
roots: (panel: HTMLElement) => HTMLElement;
|
|
}
|
|
|
|
export interface LaborLibrary {
|
|
root: 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,
|
|
label: tc("Title"),
|
|
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: (item) => {
|
|
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();
|
|
},
|
|
});
|
|
const aside = el("aside", {
|
|
className: "m02-shell__tree",
|
|
children: [
|
|
el("h3", { className: "m02-shell__title", text: tc("Library") }),
|
|
opt.roots(tree.root),
|
|
],
|
|
});
|
|
return {
|
|
root: el("div", { className: "m02-shell m02-labor", children: [aside, editor.root] }),
|
|
refresh: () => tree.refresh(),
|
|
bind: (fn) => (setDirty = fn),
|
|
};
|
|
}
|
|
|
|
export interface LaborSectionOptions {
|
|
/** 메인 칸에 화면을 띄움 — 고침 표시 손잡이를 돌려받음 */
|
|
show: (label: string, node: HTMLElement) => (dirty: boolean) => void;
|
|
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
|
|
confirmLeave: () => Promise<boolean>;
|
|
/** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */
|
|
onOpen: () => void;
|
|
make: () => LaborLibrary;
|
|
}
|
|
|
|
export interface LaborSection {
|
|
title: string;
|
|
body: HTMLElement;
|
|
open: () => Promise<void>;
|
|
/** 다른 화면을 열었음 — 고름 표시만 지움 */
|
|
clear: () => void;
|
|
}
|
|
|
|
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": "" },
|
|
});
|
|
const body = el("div", { className: "m02-side__items ui-sidebar-list", children: [row] });
|
|
let library: LaborLibrary | null = null;
|
|
let opened = false;
|
|
const mark = (on: boolean): void => {
|
|
opened = on;
|
|
row.classList.toggle("is-active", on);
|
|
};
|
|
|
|
async function open(): Promise<void> {
|
|
if (opened || !(await opt.confirmLeave())) return;
|
|
library ??= opt.make();
|
|
library.bind(opt.show(tc("Library"), library.root));
|
|
opt.onOpen();
|
|
mark(true);
|
|
await library.refresh();
|
|
}
|
|
row.addEventListener("click", () => void open());
|
|
return { title: tc("Title"), body, open, clear: () => mark(false) };
|
|
}
|