import { buildNavigationEditorTree } from '../../lib/navigation-editor-tree.js' /** * 네비게이션 항목 배열이 유효한지 검증한다. * @param {Array} items - { id, label, url, location, sortOrder, isVisible, isFolder, parentId } * @returns {{ ok: true } | { ok: false, message: string }} */ export const validateNavigationItems = (items) => { if (!Array.isArray(items) || items.length === 0) { return { ok: true } } const idSet = new Set() for (const item of items) { const id = String(item.id || '').trim() if (!id) { return { ok: false, message: '네비게이션 항목 id가 비어 있습니다.' } } if (idSet.has(id)) { return { ok: false, message: '네비게이션 항목 id가 중복되었습니다.' } } idSet.add(id) } const byId = new Map(items.map((i) => [String(i.id).trim(), i])) for (const item of items) { const id = String(item.id).trim() const loc = item.location if (loc === 'footer') { const p = item.parentId if (p != null && String(p).trim() !== '') { return { ok: false, message: '하단 네비게이션에는 하위 항목을 둘 수 없습니다.' } } } const p = item.parentId if (p != null && String(p).trim() !== '') { const pid = String(p).trim() if (!byId.has(pid)) { return { ok: false, message: '상위 메뉴 참조가 잘못되었습니다.' } } const parent = byId.get(pid) if (parent.location !== loc) { return { ok: false, message: '상위 메뉴는 같은 위치(상단/하단)에 있어야 합니다.' } } } } const maxDepth = 12 for (const item of items) { const path = new Set() let cur = item let depth = 0 while (cur) { const cid = String(cur.id).trim() if (path.has(cid)) { return { ok: false, message: '메뉴 상위 참조에 순환이 있습니다.' } } path.add(cid) const p = cur.parentId if (p == null || String(p).trim() === '') { break } const pid = String(p).trim() cur = byId.get(pid) depth += 1 if (depth > maxDepth) { return { ok: false, message: '메뉴 계층이 너무 깊습니다.' } } if (!cur) { break } } } return { ok: true } } /** * 부모가 항상 자식보다 먼저 오도록 삽입 순서를 정한다. * @param {Array} items - 네비게이션 항목 * @returns {Array} 정렬된 동일 배열의 얕은 복사 */ export const orderNavigationItemsForInsert = (items) => { const byId = new Map(items.map((i) => [String(i.id).trim(), i])) const result = [] const placed = new Set() /** * @param {Object} item - 항목 * @param {Set} stack - 순환 방지 * @returns {void} */ const place = (item, stack) => { const id = String(item.id).trim() if (placed.has(id)) { return } const p = item.parentId if (p != null && String(p).trim() !== '') { const pid = String(p).trim() if (byId.has(pid)) { if (stack.has(pid)) { return } stack.add(pid) place(byId.get(pid), stack) stack.delete(pid) } } if (!placed.has(id)) { result.push(item) placed.add(id) } } for (const item of items) { place(item, new Set()) } if (result.length !== items.length) { for (const item of items) { const id = String(item.id).trim() if (!placed.has(id)) { result.push(item) placed.add(id) } } } return result } /** * location 기준 DFS로 sort_order를 10단위로 다시 부여한다. * @param {Array} items - 전체 항목(변경됨) * @param {'primary'|'footer'} location - 위치 * @returns {void} */ export const renumberSortOrderByTree = (items, location) => { const tree = buildNavigationEditorTree(items, location) let n = 0 /** * @param {Array<{ item: Object, children: any[] }>} nodes - 노드 * @returns {void} */ const walk = (nodes) => { for (const { item, children } of nodes) { n += 1 item.sortOrder = n * 10 if (children.length) { walk(children) } } } walk(tree) } /** * 공개 API용 primary 트리(순환 참조 없음). * @param {Array} flatPrimary - location primary인 항목만 * @returns {Array} */ export const buildPublicPrimaryTree = (flatPrimary) => { const list = flatPrimary.map((row) => ({ id: row.id, label: row.label, url: row.url, isFolder: Boolean(row.isFolder), isVisible: row.isVisible !== false, sortOrder: row.sortOrder || 0, parentId: row.parentId || null })) const byId = new Map(list.map((i) => [String(i.id), { ...i, children: [] }])) const roots = [] for (const row of list) { const id = String(row.id) const node = byId.get(id) const p = row.parentId if (p && byId.has(String(p))) { byId.get(String(p)).children.push(node) } else { roots.push(node) } } /** * @param {Array} nodes - 노드 * @returns {void} */ const sortNodes = (nodes) => { nodes.sort((a, b) => a.sortOrder - b.sortOrder) for (const node of nodes) { if (node.children.length) { sortNodes(node.children) } } } sortNodes(roots) /** * @param {any} node - 노드 * @returns {Object} */ const strip = (node) => { const base = { id: node.id, label: node.label, url: node.url, isFolder: node.isFolder, isVisible: node.isVisible } if (node.children.length) { return { ...base, children: node.children.map(strip) } } return base } return roots.map(strip) }