Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.ts
T
eomsangdonandClaude Opus 5.5 59cb28ce58 feat(M02): 구조물 라이브러리 한 화면 · 뿌리 셋 · 층 권한 한 벌 · 남의 층 목록 · 해시 진입 (PLAN 27-1)
- 층 권한 한 벌 M02_Layer_Access — 나무 · 항목(Router_Items) · 양식(Router_Layers) 같은 규칙 · owner 인자 · 남의 층은 읽기만 · 읽기로 남의 층에 뼈대 안 씀
- 남의 층 목록 GET /api/m02/layers/{company|personal}/owners — 시스템 관리자 회사 목록 · 회사 관리자 같은 회사 사람
- ?layer= 페이지 분리 폐기 → 한 화면 뿌리 셋(마스터 · 회사 · 개인) · 뿌리 부품 TreePanel_Roots(펼칠 때 불러옴) · TreePanel 은 owner · api 를 받는 공용 부품
- 진입 해시 ?open=구조물조합|일위대가|옵션|도면&root=&owner= · 항목 고르기 [관리 화면에서 고치기]도 이 해시
- 「항목 나무」 → 「구조물 라이브러리」
- 시험 test_m02_layer_access 새로 · layer_screen · template_layers 새 규칙으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:19:29 +09:00

68 lines
3.1 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_TreePanel_Model.ts
* 관리 화면 구조물 라이브러리의 자료 쪽(화면 없음) — 서버 나무(`폴더` + `items`)를 폴더 나무로 · 찾기 ·
* 폴더 목록. 항목 경로는 폴더 이름 배열 + 마지막 마디가 항목 이름(PLAN 26-1 · 깊이 자유).
* ========================================================================== */
import type { TreeItem, TreeView } from "./M02_MasterTemplete_Api_Fetch";
export interface FolderNode {
/** 이 폴더까지의 경로 — 맨 위 = 빈 배열 */
path: string[];
folders: FolderNode[];
items: TreeItem[];
}
export const samePath = (a: string[], b: string[]): boolean =>
a.length === b.length && a.every((part, i) => part === b[i]);
/** `path` 가 `folder` 와 같거나 그 안인가 */
export const isUnder = (path: string[], folder: string[]): boolean =>
folder.length <= path.length && folder.every((part, i) => part === path[i]);
export const folderIdOf = (path: string[]): string => `f|${path.join("|")}`;
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 {
const words = text.trim().toLowerCase().split(/\s+/).filter(Boolean);
const hay = [...item.경로, item.key, item.일위대가 ?? ""].join(" ").toLowerCase();
return words.every((word) => hay.includes(word));
}
/** 폴더 나무 — 찾는 글이 있으면 맞는 항목과 그 위 폴더만 · 없으면 빈 폴더까지 전부 */
export function buildFolders(view: TreeView, text = ""): FolderNode {
const searching = text.trim() !== "";
const root: FolderNode = { path: [], folders: [], items: [] };
const at = (path: string[]): FolderNode =>
path.reduce((node, name, i) => {
let next = node.folders.find((f) => f.path[i] === name);
if (!next) {
next = { path: path.slice(0, i + 1), folders: [], items: [] };
node.folders.push(next);
}
return next;
}, root);
if (!searching) view.폴더.forEach(at);
for (const item of view.items) if (matches(item, text)) at(folderOf(item)).items.push(item);
return root;
}
export const countItems = (node: FolderNode): number =>
node.items.length + node.folders.reduce((sum, f) => sum + countItems(f), 0);
/** 모든 폴더 경로(맨 위 제외) — 옮길 곳 고르기 */
export function listFolders(root: FolderNode): string[][] {
return root.folders.flatMap((f) => [f.path, ...listFolders(f)]);
}
/** 그 열이 들어 있는 폴더 길 — 나무를 그 항목까지 펼칠 때 */
export function ancestorsOf(view: TreeView, colId: string): string[][] {
const item = view.items.find((one) => one.열 === colId);
if (!item) return [];
const folder = folderOf(item);
return folder.map((_, i) => folder.slice(0, i + 1));
}