export interface SystemMenu { id: string; parentId: string | null; name: string; idPath: string | null; url: string | null; roleName: string | null; sortOrder: number; isActive: boolean; isTopActive: boolean; isDefault: boolean; description: string | null; largeIconId: string | null; largeIconName: string | null; largeIconUrl: string | null; smallIconId: string | null; smallIconName: string | null; smallIconUrl: string | null; } export interface SystemMenuNode extends SystemMenu { depth: number; children: SystemMenuNode[]; } export const EMPTY_FIELD_PLACEHOLDER = '-'; export function formatOptionalValue( value: string | number | null | undefined ): string { return value === null || value === undefined || value === '' ? EMPTY_FIELD_PLACEHOLDER : String(value); } export function formatActiveLabel(isActive: boolean): string { return isActive ? '활성' : '비활성'; } /** * 평면 목록을 트리로 세운다. 부모가 목록에 없는 항목(권한 때문에 잘려 온 가지 등)은 뿌리로 * 올려 화면에서 사라지지 않게 한다. */ export function buildMenuTree(items: SystemMenu[]): SystemMenuNode[] { const byId = new Map(); for (const item of items) { byId.set(item.id, { ...item, depth: 0, children: [] }); } const roots: SystemMenuNode[] = []; for (const node of byId.values()) { const parent = node.parentId ? byId.get(node.parentId) : undefined; if (parent && parent !== node) { parent.children.push(node); } else { roots.push(node); } } function order(nodes: SystemMenuNode[], depth: number): SystemMenuNode[] { nodes.sort((a, b) => a.sortOrder - b.sortOrder || a.id.localeCompare(b.id)); for (const node of nodes) { node.depth = depth; order(node.children, depth + 1); } return nodes; } return order(roots, 0); } /** 펼쳐진 가지만 위에서부터 한 줄씩 늘어놓는다. */ export function flattenOpenNodes( nodes: SystemMenuNode[], isOpen: (id: string) => boolean ): SystemMenuNode[] { const rows: SystemMenuNode[] = []; function walk(list: SystemMenuNode[]) { for (const node of list) { rows.push(node); if (node.children.length > 0 && isOpen(node.id)) { walk(node.children); } } } walk(nodes); return rows; } export function collectNodeIds(nodes: SystemMenuNode[]): string[] { const ids: string[] = []; function walk(list: SystemMenuNode[]) { for (const node of list) { if (node.children.length > 0) { ids.push(node.id); walk(node.children); } } } walk(nodes); return ids; } /** * 새로 만들 메뉴의 `menuIdPath` — 조상 경로다(부모 경로 + 부모 id). * * 백엔드는 이 값을 받아 저장만 하고 만들어 주지 않는다(등록 SQL이 `IFNULL(#{menuIdPath}, '/')`). * 규격을 정한 곳이 없어 뿌리는 `/`, 그 아래는 `/3`, `/3/18`로 쌓는 형식을 쓴다. */ export function buildChildIdPath(parent: SystemMenu | null): string { if (!parent) { return '/'; } const base = parent.idPath && parent.idPath !== '/' ? parent.idPath : ''; return `${base}/${parent.id}`; } /** 수정·추가 팝업의 상위메뉴명 표시 — 기획의 `[3] 고객센터` 형식. */ export function formatParentLabel(parent: SystemMenu | null): string { return parent ? `[${parent.id}] ${parent.name}` : ''; }