UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

407 lines (361 loc) 9.65 kB
"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import type { CacheLevel, CacheStats, ContentType, CacheStatus, CacheMetrics, } from "./types"; // 캐시 매니저 인스턴스 (실제 구현에서는 Context에서 제공) interface CacheManagerInterface { get<T>( key: string, contentType: ContentType, options?: { levels?: CacheLevel[]; fallback?: () => Promise<T>; } ): Promise<{ data: T | null; status: CacheStatus; level?: CacheLevel }>; set<T>( key: string, data: T, contentType: ContentType, options?: { ttl?: number; levels?: CacheLevel[]; tags?: string[]; } ): Promise<boolean>; invalidate( pattern: string | string[], options?: { levels?: CacheLevel[]; strategy?: "key" | "tag" | "pattern"; } ): Promise<number>; getStats(): CacheStats; healthCheck(): Promise<{ healthy: boolean; levels: Map<CacheLevel, boolean>; issues: string[]; }>; } // 글로벌 캐시 매니저 (실제로는 Context Provider에서 제공) declare const cacheManager: CacheManagerInterface; /** * 캐시된 데이터를 조회하고 관리하는 훅 */ export function useCache<T>( key: string, contentType: ContentType, options?: { levels?: CacheLevel[]; fallback?: () => Promise<T>; enabled?: boolean; refetchInterval?: number; staleTime?: number; } ) { const [data, setData] = useState<T | null>(null); const [status, setStatus] = useState<CacheStatus>("miss"); const [level, setLevel] = useState<CacheLevel | undefined>(undefined); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [lastUpdated, setLastUpdated] = useState<Date | null>(null); const fetchTimeRef = useRef<Date | null>(null); const intervalRef = useRef<NodeJS.Timeout | null>(null); const fetchData = useCallback(async () => { if (!options?.enabled) return; setLoading(true); setError(null); try { const result = await cacheManager.get(key, contentType, { levels: options?.levels, fallback: options?.fallback, }); setData(result.data); setStatus(result.status); setLevel(result.level); setLastUpdated(new Date()); fetchTimeRef.current = new Date(); } catch (err) { setError(err instanceof Error ? err.message : "캐시 조회 오류"); setStatus("error"); } finally { setLoading(false); } }, [key, contentType, options?.levels, options?.fallback, options?.enabled]); // Stale time 체크 const isStale = useCallback(() => { if (!fetchTimeRef.current || !options?.staleTime) return false; return Date.now() - fetchTimeRef.current.getTime() > options.staleTime; }, [options?.staleTime]); // 데이터 무효화 const invalidate = useCallback(async () => { await cacheManager.invalidate(key, { levels: options?.levels }); await fetchData(); }, [key, options?.levels, fetchData]); // 데이터 설정 const setCache = useCallback( async ( newData: T, cacheOptions?: { ttl?: number; tags?: string[]; } ) => { await cacheManager.set(key, newData, contentType, { levels: options?.levels, ...cacheOptions, }); setData(newData); setLastUpdated(new Date()); fetchTimeRef.current = new Date(); }, [key, contentType, options?.levels] ); // 초기 로드 및 인터벌 설정 useEffect(() => { if (options?.enabled !== false) { fetchData(); } // 자동 갱신 설정 if (options?.refetchInterval) { intervalRef.current = setInterval(() => { if (!loading && (isStale() || !data)) { fetchData(); } }, options.refetchInterval); } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, [ fetchData, options?.refetchInterval, options?.enabled, loading, data, isStale, ]); return { data, status, level, loading, error, lastUpdated, isStale: isStale(), refetch: fetchData, invalidate, setCache, }; } /** * 캐시 통계를 조회하는 훅 */ export function useCacheStats(refreshInterval = 5000) { const [stats, setStats] = useState<CacheStats | null>(null); const [loading, setLoading] = useState(true); const fetchStats = useCallback(() => { try { const currentStats = cacheManager.getStats(); setStats(currentStats); } catch (error) { console.error("캐시 통계 조회 오류:", error); } finally { setLoading(false); } }, []); useEffect(() => { fetchStats(); const interval = setInterval(fetchStats, refreshInterval); return () => clearInterval(interval); }, [fetchStats, refreshInterval]); return { stats, loading, refresh: fetchStats, }; } /** * 캐시 헬스 체크 훅 */ export function useCacheHealth(checkInterval = 30000) { const [health, setHealth] = useState<{ healthy: boolean; levels: Map<CacheLevel, boolean>; issues: string[]; } | null>(null); const [checking, setChecking] = useState(false); const checkHealth = useCallback(async () => { setChecking(true); try { const healthResult = await cacheManager.healthCheck(); setHealth(healthResult); } catch (error) { console.error("캐시 헬스 체크 오류:", error); setHealth({ healthy: false, levels: new Map(), issues: ["헬스 체크 실행 실패"], }); } finally { setChecking(false); } }, []); useEffect(() => { checkHealth(); const interval = setInterval(checkHealth, checkInterval); return () => clearInterval(interval); }, [checkHealth, checkInterval]); return { health, checking, checkHealth, }; } /** * 여러 키를 동시에 캐시에서 조회하는 훅 */ export function useMultiCache<T>( keys: Array<{ key: string; contentType: ContentType; fallback?: () => Promise<T>; }>, options?: { levels?: CacheLevel[]; enabled?: boolean; } ) { const [results, setResults] = useState< Map< string, { data: T | null; status: CacheStatus; level?: CacheLevel; } > >(new Map()); const [loading, setLoading] = useState(false); const [errors, setErrors] = useState<Map<string, string>>(new Map()); const fetchAll = useCallback(async () => { if (!options?.enabled || keys.length === 0) return; setLoading(true); setErrors(new Map()); const newResults = new Map(); const newErrors = new Map(); await Promise.allSettled( keys.map(async ({ key, contentType, fallback }) => { try { const result = await cacheManager.get(key, contentType, { levels: options?.levels, fallback, }); newResults.set(key, result); } catch (error) { newErrors.set( key, error instanceof Error ? error.message : "오류 발생" ); } }) ); setResults(newResults); setErrors(newErrors); setLoading(false); }, [keys, options?.levels, options?.enabled]); const invalidateAll = useCallback(async () => { const keyList = keys.map(({ key }) => key); await cacheManager.invalidate(keyList, { levels: options?.levels }); await fetchAll(); }, [keys, options?.levels, fetchAll]); useEffect(() => { if (options?.enabled !== false) { fetchAll(); } }, [fetchAll, options?.enabled]); return { results, loading, errors, refetchAll: fetchAll, invalidateAll, }; } /** * 캐시 무효화를 위한 훅 */ export function useCacheInvalidation() { const [invalidating, setInvalidating] = useState(false); const invalidatePattern = useCallback( async ( pattern: string | string[], options?: { levels?: CacheLevel[]; strategy?: "key" | "tag" | "pattern"; } ) => { setInvalidating(true); try { const count = await cacheManager.invalidate(pattern, options); return count; } catch (error) { console.error("캐시 무효화 오류:", error); throw error; } finally { setInvalidating(false); } }, [] ); const invalidateByTag = useCallback( async (tags: string | string[], levels?: CacheLevel[]) => { return invalidatePattern(Array.isArray(tags) ? tags : [tags], { levels, strategy: "tag", }); }, [invalidatePattern] ); const invalidateByKey = useCallback( async (keys: string | string[], levels?: CacheLevel[]) => { return invalidatePattern(keys, { levels, strategy: "key", }); }, [invalidatePattern] ); return { invalidating, invalidatePattern, invalidateByTag, invalidateByKey, }; } /** * 실시간 캐시 메트릭 훅 */ export function useCacheMetrics(updateInterval = 1000) { const [metrics, setMetrics] = useState<CacheMetrics | null>(null); useEffect(() => { const updateMetrics = () => { try { const stats = cacheManager.getStats(); setMetrics(stats.overall); } catch (error) { console.error("메트릭 업데이트 오류:", error); } }; updateMetrics(); const interval = setInterval(updateMetrics, updateInterval); return () => clearInterval(interval); }, [updateInterval]); return metrics; }