/* ============================================================================= * ui_template_item_picker_data.ts * 항목 고르기의 자료 쪽(화면 없음) — 항목 목록 → 나무 · 찾기 · 폴더 줄 · 돌려줄 `summary_item`. * 모양은 PLAN.md 21-1 약속 그대로: 항목 목록 API 한 줄 = `PickerRow`, 참조 = `SummaryItem`. * ========================================================================== */ export type PickerLayer = "system" | "company" | "personal"; /** 21-1 `summary_item` — 구조물 기록에 붙는 항목 참조 */ export interface SummaryItem { layer: PickerLayer; owner: string | null; key: string; name: string; } /** 항목 줄이 가리키는 키 칸 — 서버가 `names` 로 이름을 함께 주면 이름을 보임 */ export type NameField = "일위대가" | "자재조합" | "도번" | "면적번" | "재료번"; /** 목록 한 줄(21-1) — 서버는 `?all=1` 을 `[{layer, owner, items}]` 로 주고, 줄마다 `layer` · `owner` 를 붙여 폄. * `열` 은 집계표 열 id(rv …) — 보이는 이름은 종류 · 분류를 이은 `itemName`. */ export interface PickerRow { key: string; 열: string; 종류?: string; 분류1?: string; 분류2?: string; 분류3?: string; 단위?: string; 일위대가?: string; 자재조합?: string; 도번?: string; 면적번?: string; 재료번?: string; /** 위 키 칸의 이름(21-9 서버가 더함) — 없으면 키를 작은 글로 보임 */ names?: Partial>; layer?: PickerLayer; owner?: string | null; } /** 그룹 차례 = 마스터 · 회사 · 개인 */ export const PICKER_LAYERS: readonly PickerLayer[] = ["system", "company", "personal"]; export interface PickNode { id: string; /** 그룹 · 종류 · 분류 이름 — 잎은 항목 이름 */ label: string; /** 마우스를 올렸을 때 — 잎은 키 */ tip?: string; /** 아래 잎 수 — 잎은 null */ count: number | null; children?: PickNode[]; /** 잎이면 그 항목 */ row?: PickerRow; } /** 「새 분류1」 같은 새로 만들 때의 자리표는 이름이 아님 — 보이는 글에서 뺌 */ const PLACEHOLDER = /^새 분류\s*\d*$/; export const shown = (part?: string): string => part && !PLACEHOLDER.test(part.trim()) ? part : ""; /** 보이는 항목 이름 — 종류 · 분류1~3 을 이음(자리표는 뺌 · 비면 열 id · 키) */ export const itemName = (row: PickerRow): string => [row.종류, row.분류1, row.분류2, row.분류3].map(shown).filter(Boolean).join(" ") || row.열 || row.key; /** 마우스를 올렸을 때 — 층 이름 + 키 */ export const rowTip = (row: PickerRow, labels: TreeLabels): string => `${labels.layers[layerOf(row)]} · ${row.key}`; export const layerOf = (row: PickerRow): PickerLayer => row.layer ?? "system"; export const ownerOf = (row: PickerRow): string | null => layerOf(row) === "system" ? null : (row.owner ?? null); /** 같은 항목의 자리표 — 층 · 주인 · 키 */ export const rowId = (row: PickerRow): string => `${layerOf(row)}|${ownerOf(row) ?? ""}|${row.key}`; export function toSummary(row: PickerRow): SummaryItem { return { layer: layerOf(row), owner: ownerOf(row), key: row.key, name: itemName(row) }; } export function isCurrent(current: SummaryItem | null, row: PickerRow): boolean { return ( !!current && current.layer === layerOf(row) && (current.owner ?? null) === ownerOf(row) && current.key === row.key ); } /** 찾기 — 이름 · 종류 · 분류 · 키 · 연결 이름에 글이 들어 있는 줄(빈 글이면 전부) */ export function filterRows(rows: PickerRow[], text: string): PickerRow[] { const words = text.trim().toLowerCase().split(/\s+/).filter(Boolean); if (!words.length) return rows; return rows.filter((row) => { const hay = [ itemName(row), row.key, row.일위대가, row.자재조합, ...Object.values(row.names ?? {}), ] .join(" ") .toLowerCase(); return words.every((word) => hay.includes(word)); }); } export interface TreeLabels { layers: Record; /** 종류 · 분류1 이 비었을 때 */ unclassified: string; } /** 그룹 → 종류 → 분류1 → 항목(잎). 서버가 준 차례를 지킴 · 빈 그룹도 한 줄 남김. */ export function buildTree(rows: PickerRow[], labels: TreeLabels): PickNode[] { return PICKER_LAYERS.map((layer) => { const mine = rows.filter((row) => layerOf(row) === layer); const kinds = new Map>(); for (const row of mine) { const kind = shown(row.종류) || labels.unclassified; const cat = shown(row.분류1) || labels.unclassified; const cats = kinds.get(kind) ?? new Map(); cats.set(cat, [...(cats.get(cat) ?? []), row]); kinds.set(kind, cats); } const kindNodes: PickNode[] = [...kinds].map(([kind, cats]) => { const catNodes: PickNode[] = [...cats].map(([cat, leaves]) => ({ id: `${layer}|${kind}|${cat}`, label: cat, count: leaves.length, children: leaves.map((row) => ({ id: rowId(row), label: itemName(row), tip: rowTip(row, labels), count: null, row, })), })); return { id: `${layer}|${kind}`, label: kind, count: catNodes.reduce((sum, node) => sum + (node.count ?? 0), 0), children: catNodes, }; }); return { id: layer, label: labels.layers[layer], count: mine.length, children: kindNodes }; }); } /** 잎까지 가는 길의 나무 줄 id — 처음부터 펼칠 자리 */ export function pathIds(row: PickerRow, labels: TreeLabels): string[] { const layer = layerOf(row); const kind = shown(row.종류) || labels.unclassified; const cat = shown(row.분류1) || labels.unclassified; return [layer, `${layer}|${kind}`, `${layer}|${kind}|${cat}`]; } export type FolderKind = "drawing" | "area" | "material" | "combo" | "unitPrice"; export interface FolderLine { kind: FolderKind; /** 서버가 준 이름 — 없으면 null(키를 작은 글로) */ name: string | null; /** 키 · 번호 — 이름이 있으면 마우스를 올릴 때, 없으면 작은 글로 */ key: string; } const FOLDER_FIELDS: readonly [FolderKind, NameField][] = [ ["drawing", "도번"], ["area", "면적번"], ["material", "재료번"], ["combo", "자재조합"], ["unitPrice", "일위대가"], ]; /** 오른쪽 폴더 모양 — 다섯 줄 모두 서버의 키 칸 + `names` 의 이름 */ export function folderLines(row: PickerRow): FolderLine[] { return FOLDER_FIELDS.map(([kind, field]) => ({ kind, name: row.names?.[field] || null, key: row[field] ?? "", })); }