Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.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

73 lines
3.5 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);
/** 찾기 규칙 하나(PLAN 29-6) — 띄어쓰기로 나눈 낱말이 모두 들어 있으면 참(AND · 대소문자 무시 · 빈 글 = 참) ·
* 구조물 조합 · 일위대가 라이브러리(나무 · 조합 목록)가 함께 씀 */
export function matchText(parts: (string | null | undefined)[], text: string): boolean {
const words = text.trim().toLowerCase().split(/\s+/).filter(Boolean);
const hay = parts.filter(Boolean).join(" ").toLowerCase();
return words.every((word) => hay.includes(word));
}
/** 나무 항목 찾기 — 이름 · 키 · 경로(폴더 · 구분 · 상세구분) · 단위 · 일위대가 키 */
export const matches = (item: TreeItem, text: string): boolean =>
matchText([...item.경로, item.이름, item.key, item.단위, item.일위대가], text);
/** 폴더 나무 — 찾는 글이 있으면 맞는 항목과 그 위 폴더만 · 없으면 빈 폴더까지 전부 */
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));
}