- 좌측 「구조물 집계 양식」 아래 「항목 나무」 카드(세 층 공용 · 깊이 자유 · 검색 · 키보드 위아래 · 펼침 · F2 · Delete)
- 항목 누름 → 표 그 열 고름 + 카드 넷 · 표에서 열 고름 → 나무 표시
- 나무 고침 POST /api/m02/layers/{층}/tree · 판 409 다시 읽고 알림 · 저장 안 한 표 고침은 저장/버림/취소 물음
- 읽기만 층은 메뉴 · 키 없음 · 나무 메뉴 자리는 sub5 의 ui_template_tree 그대로
- 시험 test_m02_treepanel 8 · test_m02_layer_screen 문구 맞춤
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c
68 lines
3.1 KiB
TypeScript
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));
|
|
}
|