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:
169
src/hooks/useMenuPolling.ts
Normal file
169
src/hooks/useMenuPolling.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 메뉴 폴링 훅
|
||||
*
|
||||
* 일정 간격으로 메뉴 변경사항을 확인하고 자동 갱신합니다.
|
||||
*
|
||||
* @see claudedocs/architecture/[PLAN-2025-12-29] dynamic-menu-refresh.md
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { refreshMenus } from '@/lib/utils/menuRefresh';
|
||||
|
||||
// 기본 폴링 간격: 30초
|
||||
const DEFAULT_POLLING_INTERVAL = 30 * 1000;
|
||||
|
||||
// 최소 폴링 간격: 10초 (서버 부하 방지)
|
||||
const MIN_POLLING_INTERVAL = 10 * 1000;
|
||||
|
||||
// 최대 폴링 간격: 5분
|
||||
const MAX_POLLING_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
interface UseMenuPollingOptions {
|
||||
/** 폴링 활성화 여부 (기본: true) */
|
||||
enabled?: boolean;
|
||||
/** 폴링 간격 (ms, 기본: 30초) */
|
||||
interval?: number;
|
||||
/** 메뉴 갱신 시 콜백 */
|
||||
onMenuUpdated?: () => void;
|
||||
/** 에러 발생 시 콜백 */
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
|
||||
interface UseMenuPollingReturn {
|
||||
/** 수동으로 메뉴 갱신 실행 */
|
||||
refresh: () => Promise<void>;
|
||||
/** 폴링 일시 중지 */
|
||||
pause: () => void;
|
||||
/** 폴링 재개 */
|
||||
resume: () => void;
|
||||
/** 현재 폴링 상태 */
|
||||
isPaused: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 폴링 훅
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // 기본 사용
|
||||
* useMenuPolling();
|
||||
*
|
||||
* // 옵션과 함께 사용
|
||||
* const { refresh, pause, resume } = useMenuPolling({
|
||||
* interval: 60000, // 1분마다
|
||||
* onMenuUpdated: () => console.log('메뉴 업데이트됨!'),
|
||||
* });
|
||||
*
|
||||
* // 수동 갱신
|
||||
* await refresh();
|
||||
* ```
|
||||
*/
|
||||
export function useMenuPolling(options: UseMenuPollingOptions = {}): UseMenuPollingReturn {
|
||||
const {
|
||||
enabled = true,
|
||||
interval = DEFAULT_POLLING_INTERVAL,
|
||||
onMenuUpdated,
|
||||
onError,
|
||||
} = options;
|
||||
|
||||
// 폴링 간격 유효성 검사
|
||||
const safeInterval = Math.max(MIN_POLLING_INTERVAL, Math.min(MAX_POLLING_INTERVAL, interval));
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isPausedRef = useRef(false);
|
||||
|
||||
// 메뉴 갱신 실행
|
||||
const executeRefresh = useCallback(async () => {
|
||||
if (isPausedRef.current) return;
|
||||
|
||||
const result = await refreshMenus();
|
||||
|
||||
if (result.success && result.updated) {
|
||||
onMenuUpdated?.();
|
||||
}
|
||||
|
||||
if (!result.success && result.error) {
|
||||
onError?.(result.error);
|
||||
}
|
||||
}, [onMenuUpdated, onError]);
|
||||
|
||||
// 수동 갱신 함수
|
||||
const refresh = useCallback(async () => {
|
||||
await executeRefresh();
|
||||
}, [executeRefresh]);
|
||||
|
||||
// 폴링 일시 중지
|
||||
const pause = useCallback(() => {
|
||||
isPausedRef.current = true;
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 폴링 재개
|
||||
const resume = useCallback(() => {
|
||||
isPausedRef.current = false;
|
||||
if (enabled && !intervalRef.current) {
|
||||
intervalRef.current = setInterval(executeRefresh, safeInterval);
|
||||
}
|
||||
}, [enabled, safeInterval, executeRefresh]);
|
||||
|
||||
// 폴링 설정
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 페이지 로드 시 즉시 실행하지 않음 (로그인 시 이미 받아옴)
|
||||
// 폴링 시작
|
||||
intervalRef.current = setInterval(executeRefresh, safeInterval);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [enabled, safeInterval, executeRefresh]);
|
||||
|
||||
// 탭 가시성 변경 시 처리
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
// 탭이 숨겨지면 폴링 중지 (리소스 절약)
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
} else {
|
||||
// 탭이 다시 보이면 즉시 갱신 후 폴링 재개
|
||||
if (!isPausedRef.current) {
|
||||
executeRefresh();
|
||||
intervalRef.current = setInterval(executeRefresh, safeInterval);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [enabled, safeInterval, executeRefresh]);
|
||||
|
||||
return {
|
||||
refresh,
|
||||
pause,
|
||||
resume,
|
||||
isPaused: isPausedRef.current,
|
||||
};
|
||||
}
|
||||
|
||||
export default useMenuPolling;
|
||||
Reference in New Issue
Block a user