feat: 메뉴 폴링 및 문서 업데이트
- 메뉴 폴링 API 및 훅 추가 (useMenuPolling, menuRefresh) - AuthenticatedLayout 메뉴 새로고침 연동 - 품질검사 체크리스트 문서 추가 - Vercel 배포 가이드 추가 - 동적 메뉴 리프레시 계획 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
189
src/lib/utils/menuRefresh.ts
Normal file
189
src/lib/utils/menuRefresh.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 메뉴 동적 갱신 유틸리티
|
||||
*
|
||||
* 관리자가 게시판/메뉴 추가 시 재로그인 없이 메뉴를 갱신합니다.
|
||||
*
|
||||
* @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; // 실제로 메뉴가 변경되었는지
|
||||
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/menus', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// 401 등 인증 오류는 조용히 실패 (로그아웃 상태일 수 있음)
|
||||
if (response.status === 401) {
|
||||
return { success: false, updated: false };
|
||||
}
|
||||
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) {
|
||||
const parsed = JSON.parse(userData);
|
||||
parsed.menu = transformedMenus;
|
||||
localStorage.setItem('user', JSON.stringify(parsed));
|
||||
}
|
||||
|
||||
// 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/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) {
|
||||
const parsed = JSON.parse(userData);
|
||||
parsed.menu = transformedMenus;
|
||||
localStorage.setItem('user', JSON.stringify(parsed));
|
||||
}
|
||||
|
||||
// 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 : '알 수 없는 오류'
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user