Files
sam-react-prod/src/lib/utils/menuRefresh.ts

199 lines
5.3 KiB
TypeScript
Raw Normal View History

/**
*
*
* / .
*
* @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<RefreshMenuResult> {
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<RefreshMenuResult> {
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 : '알 수 없는 오류'
};
}
}