- ui_template_tree: M01 renderTree 를 공용으로 올림(M01 은 같은 이름으로 그대로 부름) · 기본 줄 createTreeRow · 클래스 ui-tree__*
- ui_template_item_picker: openItemPicker({current, canCopy}) → apply(summary_item) | clear | cancel · 왼쪽 마스터 · 회사 · 개인 → 종류 → 분류1 → 항목 나무 + 찾기 · 오른쪽 폴더 모양(키는 툴팁) · 개인용으로 복사 · 관리 화면 이동
- 목록 GET /api/m02/items?all=1 · 복사 POST /api/m02/layers/personal/items/copy — 서버가 없으면(404) 21-1 모양 가짜 응답
- 자료 쪽(나무 · 찾기 · 폴더 줄)은 화면 없는 모듈로 나눠 시험
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7JS47SacjPxZ718QsZKyr
162 lines
5.5 KiB
TypeScript
162 lines
5.5 KiB
TypeScript
/* =============================================================================
|
|
* 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;
|
|
}
|
|
|
|
/** `GET /api/m02/items` 한 줄(21-1) — `?all=1` 은 줄마다 `layer` · `owner` 가 붙음 */
|
|
export interface PickerRow {
|
|
key: string;
|
|
열: string;
|
|
종류?: string;
|
|
분류1?: string;
|
|
분류2?: string;
|
|
분류3?: string;
|
|
단위?: string;
|
|
일위대가?: string;
|
|
자재조합?: string;
|
|
도번?: string;
|
|
면적번?: string;
|
|
재료번?: string;
|
|
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;
|
|
}
|
|
|
|
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: 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 = [
|
|
row.열,
|
|
row.종류,
|
|
row.분류1,
|
|
row.분류2,
|
|
row.분류3,
|
|
row.key,
|
|
row.일위대가,
|
|
row.자재조합,
|
|
]
|
|
.join(" ")
|
|
.toLowerCase();
|
|
return words.every((word) => hay.includes(word));
|
|
});
|
|
}
|
|
|
|
export interface TreeLabels {
|
|
layers: Record<PickerLayer, string>;
|
|
/** 종류 · 분류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<string, Map<string, PickerRow[]>>();
|
|
for (const row of mine) {
|
|
const kind = row.종류 || labels.unclassified;
|
|
const cat = row.분류1 || labels.unclassified;
|
|
const cats = kinds.get(kind) ?? new Map<string, PickerRow[]>();
|
|
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: row.열,
|
|
tip: row.key,
|
|
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 = row.종류 || labels.unclassified;
|
|
const cat = row.분류1 || labels.unclassified;
|
|
return [layer, `${layer}|${kind}`, `${layer}|${kind}|${cat}`];
|
|
}
|
|
|
|
export type FolderKind = "drawing" | "area" | "material" | "combo" | "unitPrice";
|
|
|
|
export interface FolderLine {
|
|
kind: FolderKind;
|
|
/** 보이는 이름 */
|
|
text: string;
|
|
/** 마우스를 올렸을 때 — 키 · 번호 */
|
|
tip: string;
|
|
}
|
|
|
|
/** 오른쪽 폴더 모양 — 도면 · 면적산출 · 재료산출은 그 항목(열) 이름 · 자재조합 · 일위대가는 서버 값 */
|
|
export function folderLines(row: PickerRow): FolderLine[] {
|
|
return [
|
|
{ kind: "drawing", text: row.열, tip: row.도번 ?? "" },
|
|
{ kind: "area", text: row.열, tip: row.면적번 ?? "" },
|
|
{ kind: "material", text: row.열, tip: row.재료번 ?? "" },
|
|
{ kind: "combo", text: row.자재조합 ?? "", tip: row.자재조합 ?? "" },
|
|
{ kind: "unitPrice", text: row.일위대가 ?? "", tip: row.일위대가 ?? "" },
|
|
];
|
|
}
|