2025-12-29 14:54:27 +09:00
|
|
|
/**
|
|
|
|
|
* 메뉴 동적 갱신 유틸리티
|
|
|
|
|
*
|
|
|
|
|
* 관리자가 게시판/메뉴 추가 시 재로그인 없이 메뉴를 갱신합니다.
|
|
|
|
|
*
|
|
|
|
|
* @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; // 실제로 메뉴가 변경되었는지
|
2025-12-31 18:40:50 +09:00
|
|
|
sessionExpired?: boolean; // 세션 만료 여부 (401 응답)
|
2025-12-29 14:54:27 +09:00
|
|
|
error?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 메뉴 갱신 함수
|
|
|
|
|
*
|
|
|
|
|
* 1. API에서 새 메뉴 받아오기
|
|
|
|
|
* 2. 기존 메뉴와 비교 (해시)
|
|
|
|
|
* 3. 변경 시 localStorage + Zustand 업데이트
|
|
|
|
|
*
|
|
|
|
|
* @returns 갱신 결과
|
|
|
|
|
*/
|
|
|
|
|
export async function refreshMenus(): Promise<RefreshMenuResult> {
|
|
|
|
|
try {
|
|
|
|
|
// 1. 현재 메뉴 해시 저장
|
|
|
|
|
const currentHash = getCurrentMenuHash();
|
|
|
|
|
|
|
|
|
|
// 2. API에서 새 메뉴 받아오기
|
2026-01-16 15:19:09 +09:00
|
|
|
const response = await fetch('/api/proxy/menus', {
|
2025-12-29 14:54:27 +09:00
|
|
|
method: 'GET',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2025-12-31 18:40:50 +09:00
|
|
|
// 401 인증 오류 → 세션 만료로 판단
|
2025-12-29 14:54:27 +09:00
|
|
|
if (response.status === 401) {
|
2025-12-31 18:40:50 +09:00
|
|
|
console.log('[Menu] 401 응답 - 세션 만료');
|
|
|
|
|
return { success: false, updated: false, sessionExpired: true };
|
2025-12-29 14:54:27 +09:00
|
|
|
}
|
|
|
|
|
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) {
|
2026-02-09 16:14:06 +09:00
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(userData);
|
|
|
|
|
parsed.menu = transformedMenus;
|
|
|
|
|
localStorage.setItem('user', JSON.stringify(parsed));
|
|
|
|
|
} catch {
|
|
|
|
|
// localStorage 데이터 손상 시 무시
|
|
|
|
|
}
|
2025-12-29 14:54:27 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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<RefreshMenuResult> {
|
|
|
|
|
try {
|
2026-01-16 15:19:09 +09:00
|
|
|
const response = await fetch('/api/proxy/menus', {
|
2025-12-29 14:54:27 +09:00
|
|
|
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) {
|
2026-02-09 16:14:06 +09:00
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(userData);
|
|
|
|
|
parsed.menu = transformedMenus;
|
|
|
|
|
localStorage.setItem('user', JSON.stringify(parsed));
|
|
|
|
|
} catch {
|
|
|
|
|
// localStorage 데이터 손상 시 무시
|
|
|
|
|
}
|
2025-12-29 14:54:27 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 : '알 수 없는 오류'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|