feat(M02): 일위대가 사전조합 — 층 단추 따로 · 명시 owner · 구분 › 상세구분 2단 나무(134-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
This commit is contained in:
@@ -1,64 +1,182 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_UI_Side_Precombo.ts
|
||||
* 좌측 「일위대가 조합」 컨테이너(50-3 ③ · 51-4) — 구조물 조합 아래 · 층 단추(페이지가 만듦) ·
|
||||
* 항목 둘 「단가산출 승격」(promote) · 「일위대가 사전조합」(precombo)만.
|
||||
* 항목 누름 = 상세 칸에 그 종류 목록표(`Library_List` · 생성 · 복사 · 삭제는 거기) ·
|
||||
* 표 줄 누름 = 편집 칸(UI_Main 종류 promote · precombo = 일위대가 상세 모드).
|
||||
* 좌측 「일위대가 사전조합」 컨테이너(50-3 ③ · 51-4 · 134-3) — 구조물 조합 아래 · 층 단추(페이지가 만듦 ·
|
||||
* 페이지 층과 따로 · 마스터 · 회사 · 내 개인) + 그 층 사전조합 나무(구분 › 상세구분 2단 폴더 · 빈 구분 = 미분류).
|
||||
* 문서 누름 = 편집 칸(UI_Main 종류 precombo = 일위대가 상세 모드) · 켜진 층 단추 다시 누름 = 목록표
|
||||
* (`Library_List` · 생성 · 복사 · 삭제는 거기) · 그 층 주인(`owner`) · 고칠 권한(`canWrite`)은 고름에 실어 보냄.
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import type { Kind, Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { createTreeRow, renderTree, type TreeNode } from "@ui/ui_template_tree";
|
||||
import { listTemplates, type Kind, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { Root } from "./M02_MasterTemplete_Layer";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
export type PrecomboKind = Extract<Kind, "precombo">;
|
||||
|
||||
const PARTS = [{ kind: "precombo", label: "Lib_PrecomboAll" }] as const;
|
||||
export const PRECOMBO_TITLE = "일위대가 조합";
|
||||
export const PRECOMBO_TITLE = "일위대가 사전조합";
|
||||
export const isPrecombo = (kind: Kind): kind is PrecomboKind => kind === "precombo";
|
||||
|
||||
export interface PrecomboOptions {
|
||||
/** 사전조합 층(134-3) — 페이지 층과 따로 · `owner` null = 자기 층 */
|
||||
export interface PrecomboScope {
|
||||
root: Root;
|
||||
layer: Layer;
|
||||
/** 층 단추 줄(페이지가 만듦 · 누르면 그 층 화면 + 이 컨테이너 펼침) */
|
||||
bar: HTMLElement[];
|
||||
owner: string | null;
|
||||
canWrite: boolean;
|
||||
}
|
||||
|
||||
/** 그 층 목록표 고름(켜진 층 단추 다시 누름) */
|
||||
export const precomboList = (scope: PrecomboScope): Selection => ({
|
||||
layer: scope.layer,
|
||||
owner: scope.owner,
|
||||
canWrite: scope.canWrite,
|
||||
projectId: null,
|
||||
kind: "precombo",
|
||||
name: "",
|
||||
label: tc("Lib_PrecomboAll"),
|
||||
});
|
||||
|
||||
export interface PrecomboOptions {
|
||||
scope: () => PrecomboScope;
|
||||
/** 층 단추 줄(페이지가 만듦 · 그릴 때마다 새로 — 켜진 단추가 바뀜) */
|
||||
bar: () => HTMLElement;
|
||||
confirmLeave: () => Promise<boolean>;
|
||||
onOpen: (sel: Selection | null) => void;
|
||||
/** 이 층에서 지금 열린 종류(목록이든 문서든) — 항목 고름 표시 */
|
||||
openKind: () => Kind | null;
|
||||
getOpen: () => Selection | null;
|
||||
}
|
||||
|
||||
export interface PrecomboHandle {
|
||||
body: HTMLElement;
|
||||
/** 고름 표시만 */
|
||||
paint: () => void;
|
||||
/** 층 단추 · 나무를 그 층 목록으로 다시 그림 */
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface Row {
|
||||
이름: string;
|
||||
종류: string;
|
||||
제목?: string | null;
|
||||
구분?: string | null;
|
||||
상세구분?: string | null;
|
||||
}
|
||||
|
||||
/** 줄 → 구분 › 상세구분 2단 나무(빈 구분 = 미분류 · 맨 뒤 · 빈 상세구분 = 구분 바로 아래) */
|
||||
export function groupRows(
|
||||
rows: Row[],
|
||||
unsorted: string,
|
||||
): { 구분: string; 상세: Map<string, Row[]> }[] {
|
||||
const out = new Map<string, Map<string, Row[]>>();
|
||||
for (const row of rows) {
|
||||
const top = row.구분 || "";
|
||||
const sub = row.구분 ? row.상세구분 || "" : "";
|
||||
if (!out.has(top)) out.set(top, new Map());
|
||||
const subs = out.get(top)!;
|
||||
subs.set(sub, [...(subs.get(sub) ?? []), row]);
|
||||
}
|
||||
const order = (a: string, b: string): number =>
|
||||
!a ? 1 : !b ? -1 : a.localeCompare(b, "ko-KR", { numeric: true });
|
||||
return [...out.keys()].sort(order).map((top) => ({
|
||||
구분: top || unsorted,
|
||||
상세: new Map([...out.get(top)!].sort(([a], [b]) => order(a, b))),
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle {
|
||||
const items = PARTS.map((part) => {
|
||||
const b = el("button", {
|
||||
className: "m02-side__item ui-sidebar-row",
|
||||
text: tc(part.label),
|
||||
attrs: { type: "button", "data-precombo": part.kind },
|
||||
const barHost = el("div");
|
||||
const treeHost = el("div", { className: "ui-tree", attrs: { "data-precombo-tree": "" } });
|
||||
const leaves = new Map<string, HTMLElement>();
|
||||
const closed = new Set<string>(); // 접은 폴더 — 다시 그려도 그대로
|
||||
let rows: Row[] = [];
|
||||
let seq = 0;
|
||||
|
||||
const open = async (row: Row): Promise<void> => {
|
||||
const at = opt.scope();
|
||||
if (!(await opt.confirmLeave())) return;
|
||||
opt.onOpen({
|
||||
...precomboList(at),
|
||||
name: row.이름,
|
||||
label: row.제목 ? `${row.이름} ${row.제목}` : row.이름,
|
||||
});
|
||||
// 이름 "" = 목록표(UI_Main)
|
||||
b.addEventListener("click", async () => {
|
||||
if (!(await opt.confirmLeave())) return;
|
||||
opt.onOpen({
|
||||
layer: opt.layer,
|
||||
projectId: null,
|
||||
kind: part.kind,
|
||||
name: "",
|
||||
label: tc(part.label),
|
||||
});
|
||||
});
|
||||
return b;
|
||||
};
|
||||
const leaf = (row: Row): TreeNode => ({
|
||||
id: `doc:${row.이름}`,
|
||||
label: row.제목 || row.이름,
|
||||
tip: row.이름,
|
||||
count: null,
|
||||
run: () => void open(row),
|
||||
});
|
||||
const folder = (id: string, label: string, children: TreeNode[]): TreeNode => ({
|
||||
id,
|
||||
label,
|
||||
count: children.length,
|
||||
folder: true,
|
||||
open: !closed.has(id),
|
||||
children,
|
||||
});
|
||||
|
||||
const draw = (): void => {
|
||||
leaves.clear();
|
||||
if (!rows.length)
|
||||
return void treeHost.replaceChildren(
|
||||
el("p", { className: "m02-side__empty", text: tc("Lib_None") }),
|
||||
);
|
||||
const nodes = groupRows(rows, tc("Lib_Unsorted")).map((g) =>
|
||||
folder(
|
||||
`sub:${g.구분}`,
|
||||
g.구분,
|
||||
[...g.상세].flatMap(([sub, list]) =>
|
||||
sub ? [folder(`sub:${g.구분}/${sub}`, sub, list.map(leaf))] : list.map(leaf),
|
||||
),
|
||||
),
|
||||
);
|
||||
treeHost.replaceChildren(
|
||||
...renderTree(nodes, (node, caret, depth, onClick) => {
|
||||
const row = createTreeRow(node, caret, depth, () => {
|
||||
onClick();
|
||||
if (!node.children?.length) return;
|
||||
if (row.querySelector(".ui-tree__caret")?.textContent === "▸") closed.add(node.id);
|
||||
else closed.delete(node.id);
|
||||
});
|
||||
if (node.run) {
|
||||
row.dataset.precomboKey = node.id.slice(4);
|
||||
leaves.set(node.id.slice(4), row);
|
||||
}
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
paint();
|
||||
};
|
||||
|
||||
const paint = (): void => {
|
||||
const cur = opt.getOpen();
|
||||
const at = opt.scope();
|
||||
const mine =
|
||||
cur && isPrecombo(cur.kind) && cur.layer === at.layer && (cur.owner ?? null) === at.owner;
|
||||
for (const [key, row] of leaves) row.classList.toggle("is-active", !!mine && cur.name === key);
|
||||
};
|
||||
|
||||
const refresh = async (): Promise<void> => {
|
||||
const mine = ++seq;
|
||||
const at = opt.scope();
|
||||
barHost.replaceChildren(opt.bar());
|
||||
treeHost.replaceChildren(el("p", { className: "m02-side__empty", text: tc("Loading") }));
|
||||
let got: Row[] = [];
|
||||
try {
|
||||
got = (await listTemplates(at.layer, null, at.owner)).filter((r) => r.종류 === "precombo");
|
||||
} catch {
|
||||
got = []; // 못 받으면 빈 나무(목록표에서 까닭)
|
||||
}
|
||||
if (mine !== seq) return;
|
||||
rows = got;
|
||||
draw();
|
||||
};
|
||||
|
||||
const body = el("div", {
|
||||
className: "m02-side__items ui-sidebar-list",
|
||||
children: [...opt.bar, ...items],
|
||||
attrs: { "data-precombo": "" },
|
||||
children: [barHost, treeHost],
|
||||
});
|
||||
const paint = (): void => {
|
||||
const on = opt.openKind();
|
||||
items.forEach((b, i) => b.classList.toggle("is-active", PARTS[i].kind === on));
|
||||
};
|
||||
return { body, paint };
|
||||
return { body, paint, refresh };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user