/** * 메뉴 폴링 훅 * * 일정 간격으로 메뉴 변경사항을 확인하고 자동 갱신합니다. * * @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; /** 폴링 일시 중지 */ 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(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;