/** * 메뉴 동적 갱신 유틸리티 * * 관리자가 게시판/메뉴 추가 시 재로그인 없이 메뉴를 갱신합니다. * * @see claudedocs/architecture/[PLAN-2025-12-29] dynamic-menu-refresh.md */ import { transformApiMenusToMenuItems, deserializeMenuItems } from './menuTransform'; import { useMenuStore } from '@/store/menuStore'; import type { SerializableMenuItem } from '@/store/menuStore'; /** * 메뉴 해시 생성 (변경 감지용) * 메뉴 ID들을 정렬하여 해시 생성 */ function generateMenuHash(menus: SerializableMenuItem[]): string { const collectIds = (items: SerializableMenuItem[]): string[] => { return items.flatMap(item => [ item.id, ...(item.children ? collectIds(item.children) : []) ]); }; return collectIds(menus).sort().join(','); } /** * 현재 저장된 메뉴 해시 가져오기 */ export function getCurrentMenuHash(): string { if (typeof window === 'undefined') return ''; try { const userData = localStorage.getItem('user'); if (!userData) return ''; const parsed = JSON.parse(userData); if (!parsed.menu || !Array.isArray(parsed.menu)) return ''; return generateMenuHash(parsed.menu); } catch { return ''; } } /** * 메뉴 갱신 결과 타입 */ interface RefreshMenuResult { success: boolean; updated: boolean; // 실제로 메뉴가 변경되었는지 sessionExpired?: boolean; // 세션 만료 여부 (401 응답) error?: string; } /** * 메뉴 갱신 함수 * * 1. API에서 새 메뉴 받아오기 * 2. 기존 메뉴와 비교 (해시) * 3. 변경 시 localStorage + Zustand 업데이트 * * @returns 갱신 결과 */ export async function refreshMenus(): Promise { try { // 1. 현재 메뉴 해시 저장 const currentHash = getCurrentMenuHash(); // 2. API에서 새 메뉴 받아오기 const response = await fetch('/api/proxy/menus', { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { // 401 인증 오류 → 세션 만료로 판단 if (response.status === 401) { console.log('[Menu] 401 응답 - 세션 만료'); return { success: false, updated: false, sessionExpired: true }; } return { success: false, updated: false, error: `API 오류: ${response.status}` }; } const data = await response.json(); if (!data.menus || !Array.isArray(data.menus)) { return { success: false, updated: false, error: '메뉴 데이터 형식 오류' }; } // 3. 메뉴 변환 const transformedMenus = transformApiMenusToMenuItems(data.menus); const newHash = generateMenuHash(transformedMenus); // 4. 변경 없으면 업데이트 스킵 if (currentHash === newHash) { return { success: true, updated: false }; } // 5. localStorage 업데이트 (새로고침 대응) const userData = localStorage.getItem('user'); if (userData) { try { const parsed = JSON.parse(userData); parsed.menu = transformedMenus; localStorage.setItem('user', JSON.stringify(parsed)); } catch { // localStorage 데이터 손상 시 무시 } } // 6. Zustand 스토어 업데이트 (UI 즉시 반영) const { setMenuItems } = useMenuStore.getState(); setMenuItems(deserializeMenuItems(transformedMenus)); console.log('[Menu] 메뉴 갱신 완료 - 변경 감지됨'); return { success: true, updated: true }; } catch (error) { console.error('[Menu] 메뉴 갱신 실패:', error); return { success: false, updated: false, error: error instanceof Error ? error.message : '알 수 없는 오류' }; } } /** * 메뉴 강제 갱신 (비교 없이 무조건 갱신) */ export async function forceRefreshMenus(): Promise { try { const response = await fetch('/api/proxy/menus', { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { return { success: false, updated: false, error: `API 오류: ${response.status}` }; } const data = await response.json(); if (!data.menus || !Array.isArray(data.menus)) { return { success: false, updated: false, error: '메뉴 데이터 형식 오류' }; } const transformedMenus = transformApiMenusToMenuItems(data.menus); // localStorage 업데이트 const userData = localStorage.getItem('user'); if (userData) { try { const parsed = JSON.parse(userData); parsed.menu = transformedMenus; localStorage.setItem('user', JSON.stringify(parsed)); } catch { // localStorage 데이터 손상 시 무시 } } // Zustand 스토어 업데이트 const { setMenuItems } = useMenuStore.getState(); setMenuItems(deserializeMenuItems(transformedMenus)); console.log('[Menu] 메뉴 강제 갱신 완료'); return { success: true, updated: true }; } catch (error) { console.error('[Menu] 메뉴 강제 갱신 실패:', error); return { success: false, updated: false, error: error instanceof Error ? error.message : '알 수 없는 오류' }; } }