export type AdminMenu = { id: string; parentId: string | null; name: string; url: string; description: string; sortOrder: number; roleId: string; roleName: string; isActive: boolean; isTopActive: boolean; }; export type AdminMenuNode = AdminMenu & { depth: number; children: AdminMenuNode[]; }; export const ADMIN_MENU_ROOT_PATH = '/admins/menus'; export function toYesNo(value: boolean): string { return value ? 'Y' : 'N'; } export const ACTIVE_OPTIONS = [ { value: 'true', label: '활성' }, { value: 'false', label: '비활성' }, ] as const; export const NEW_WINDOW_OPTIONS = [ { value: 'true', label: '예' }, { value: 'false', label: '아니오' }, ] as const; export function formatActiveLabel(isActive: boolean): string { return isActive ? '활성' : '비활성'; } /** * 백엔드가 준 순서(메뉴 등록순)를 유지한 채 `parentId`로만 중첩한다. * 부모가 목록에 없는 행은 최상위로 올려 화면에서 사라지지 않게 한다. */ export function buildAdminMenuTree(menus: AdminMenu[]): AdminMenuNode[] { const nodes = new Map(); for (const menu of menus) { nodes.set(menu.id, { ...menu, depth: 0, children: [] }); } const roots: AdminMenuNode[] = []; for (const menu of menus) { const node = nodes.get(menu.id); if (!node) continue; const parent = menu.parentId ? nodes.get(menu.parentId) : undefined; if (parent && parent !== node) { parent.children.push(node); } else { roots.push(node); } } const assignDepth = (list: AdminMenuNode[], depth: number) => { for (const node of list) { node.depth = depth; assignDepth(node.children, depth + 1); } }; assignDepth(roots, 0); return roots; } /** 트리를 화면이 그릴 순서(부모 바로 뒤에 자식)로 편다. 접힌 가지는 건너뛴다. */ export function flattenAdminMenuTree( roots: AdminMenuNode[], collapsedIds: ReadonlySet ): AdminMenuNode[] { const rows: AdminMenuNode[] = []; const walk = (nodes: AdminMenuNode[]) => { for (const node of nodes) { rows.push(node); if (!collapsedIds.has(node.id)) { walk(node.children); } } }; walk(roots); return rows; }