Files
sam-react-prod/src/hooks/useMenuPolling.ts

169 lines
4.3 KiB
TypeScript
Raw Normal View History

/**
*
*
* .
*
* @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;