// hooks/use-sync-status.ts (완전히 개선된 버전) import useSWR, { mutate as globalMutate } from 'swr' import useSWRMutation from 'swr/mutation' import * as React from 'react' // 🔧 타입 정의 강화 interface SyncStatus { syncEnabled: boolean pendingChanges: number syncedChanges: number failedChanges: number lastSyncAt?: string | null error?: string | null projectId?: number targetSystem?: string lastUpdated?: string } interface ApiError extends Error { status: number response?: any } interface SyncBatch { id: string projectId: number targetSystem: string status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' startedAt?: string completedAt?: string itemCount: number successCount: number failureCount: number errors?: string[] } interface SyncConfig { projectId: number targetSystem: string autoSyncEnabled: boolean syncInterval: number batchSize: number retryAttempts: number notificationEnabled: boolean } // 🔧 환경 설정 const MAX_CONTRACTS = parseInt(process.env.NEXT_PUBLIC_MAX_SYNC_CONTRACTS || '20', 10) // 🔧 개선된 fetcher 함수 const fetcher = async (url: string): Promise => { try { const response = await fetch(url, { headers: { 'Content-Type': 'application/json', } }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) const error = new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`) as ApiError error.status = response.status error.response = errorData throw error } return response.json() } catch (error) { if (error instanceof TypeError && error.message === 'Failed to fetch') { // 네트워크 오류 const networkError = new Error('네트워크 연결을 확인해주세요') as ApiError networkError.status = 0 throw networkError } throw error } } // 🔧 안전한 기본값 생성 const createDefaultSyncStatus = (error?: string, projectId?: number): SyncStatus => ({ syncEnabled: false, pendingChanges: 0, syncedChanges: 0, failedChanges: 0, lastSyncAt: null, error, projectId, lastUpdated: new Date().toISOString() }) // ✅ 단일 계약 동기화 상태 조회 export function useSyncStatus(projectId: number | null, targetSystem: string = 'SHI') { const key = projectId ? `/api/sync/status?projectId=${projectId}&targetSystem=${targetSystem}` : null const { data, error, isLoading, mutate: localMutate } = useSWR( key, fetcher, { refreshInterval: 30000, revalidateOnFocus: true, revalidateOnReconnect: true, shouldRetryOnError: (error: ApiError) => { // 4xx 에러는 재시도하지 않음 return error.status >= 500 || error.status === 0 }, errorRetryCount: 3, errorRetryInterval: 2000, dedupingInterval: 5000, keepPreviousData: true, // 서버 사이드 렌더링에서는 요청하지 않음 isPaused: () => typeof window === 'undefined' } ) const refetch = React.useCallback(() => { if (key) { localMutate() } }, [key, localMutate]) // 항상 안전한 데이터 반환 const safeData: SyncStatus = React.useMemo(() => { if (data && typeof data === 'object') { return { syncEnabled: Boolean(data.syncEnabled), pendingChanges: Number(data.pendingChanges) || 0, syncedChanges: Number(data.syncedChanges) || 0, failedChanges: Number(data.failedChanges) || 0, lastSyncAt: data.lastSyncAt || null, error: data.error || (error ? (error as ApiError).message : null), projectId: projectId || data.projectId, targetSystem: targetSystem, lastUpdated: new Date().toISOString() } } return createDefaultSyncStatus( error ? (error as ApiError).message : undefined, projectId || undefined ) }, [data, error, projectId, targetSystem]) return { syncStatus: safeData, isLoading: isLoading && Boolean(projectId), error: error as ApiError | null, refetch } } // ❌ useMultipleSyncStatus 제거 (Hook 규칙 위반 때문에) // 대신 useDynamicSyncStatus 사용 권장 // ✅ 다중 계약 동기화 상태 조회 (Hook 규칙 준수) export function useDynamicSyncStatus(projectIds: number[], targetSystem: string = 'SHI') { // Hook 규칙 준수: 고정된 수의 Hook 호출 const paddedContractIds = React.useMemo(() => { // 입력 검증 및 경고 if (projectIds.length > MAX_CONTRACTS) { console.warn(`Contract count (${projectIds.length}) exceeds maximum (${MAX_CONTRACTS}). Only first ${MAX_CONTRACTS} will be processed.`) } // 🔥 명시적 타입 선언 const padded: (number | null)[] = [...projectIds.slice(0, MAX_CONTRACTS)] // null로 패딩 while (padded.length < MAX_CONTRACTS) { padded.push(null) } return padded }, [projectIds]) // 각 contractId에 대해 고정된 수의 Hook 호출 const allResults = paddedContractIds.map((projectId) => { const result = useSyncStatus(projectId, targetSystem) return projectId ? { projectId, ...result } : null }) // 유효한 결과만 필터링 const validResults = React.useMemo(() => { return allResults.filter((result): result is { projectId: number syncStatus: SyncStatus isLoading: boolean error: ApiError | null refetch: () => void } => result !== null) }, [allResults]) // 전체 통계 계산 const totalStats = React.useMemo(() => { let totalPending = 0 let totalSynced = 0 let totalFailed = 0 let hasError = false let isLoading = false validResults.forEach(({ syncStatus, error, isLoading: loading }) => { if (error) hasError = true if (loading) isLoading = true if (syncStatus && typeof syncStatus === 'object') { totalPending += Number(syncStatus.pendingChanges) || 0 totalSynced += Number(syncStatus.syncedChanges) || 0 totalFailed += Number(syncStatus.failedChanges) || 0 } }) return { totalPending, totalSynced, totalFailed, hasError, isLoading, canSync: totalPending > 0 && !hasError && projectIds.length > 0 } }, [validResults, projectIds.length]) const refetchAll = React.useCallback(() => { validResults.forEach(({ refetch }) => { try { refetch() } catch (error) { console.warn('Failed to refetch sync status:', error) } }) }, [validResults]) return { contractStatuses: validResults, totalStats, refetchAll } } // ✅ 클라이언트 전용 동기화 상태 조회 (서버 사이드 렌더링 호환) export function useClientSyncStatus(projectIds: number[], targetSystem: string = 'SHI',) { const [isClient, setIsClient] = React.useState(false) React.useEffect(() => { setIsClient(true) }, []) const syncResult = useDynamicSyncStatus( isClient ? projectIds : [], targetSystem ) // 서버 사이드에서는 안전한 기본값 반환 if (!isClient) { return { contractStatuses: [], totalStats: { totalPending: 0, totalSynced: 0, totalFailed: 0, hasError: false, isLoading: true, canSync: false }, refetchAll: () => {} } } return syncResult } // ✅ 동기화 배치 목록 조회 export function useSyncBatches(projectId: number | null, targetSystem: string = 'SHI') { const key = projectId ? `/api/sync/batches?projectId=${projectId}&targetSystem=${targetSystem}` : null const { data, error, isLoading } = useSWR( key, fetcher, { revalidateOnFocus: false, shouldRetryOnError: false, refreshInterval: 10000, // 10초마다 갱신 } ) return { syncBatches: data || [], isLoading: isLoading && Boolean(projectId), error: error as ApiError | null } } // ✅ 동기화 설정 조회 export function useSyncConfig(projectId: number | null, targetSystem: string = 'SHI') { const key = projectId ? `/api/sync/config?projectId=${projectId}&targetSystem=${targetSystem}` : null const { data, error, isLoading, mutate: localMutate } = useSWR( key, fetcher, { revalidateOnFocus: false, shouldRetryOnError: false, } ) const refetch = React.useCallback(() => { if (key) { localMutate() } }, [key, localMutate]) return { syncConfig: data, isLoading: isLoading && Boolean(projectId), error: error as ApiError | null, refetch } } // ✅ 동기화 트리거 (뮤테이션) export function useTriggerSync() { const { trigger, isMutating, error } = useSWRMutation( '/api/sync/trigger', async (url: string, { arg }: { arg: { projectId: number; targetSystem?: string } }) => { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(arg) }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) const error = new Error(errorData.message || `동기화 실패: HTTP ${response.status}`) as ApiError error.status = response.status error.response = errorData throw error } return response.json() } ) const triggerSync = React.useCallback(async (arg: { projectId: number; targetSystem?: string }) => { try { const result = await trigger(arg) // 성공 후 관련 캐시 무효화 const targetSystem = arg.targetSystem || 'SHI' const statusKey = `/api/sync/status?projectId=${arg.projectId}&targetSystem=${targetSystem}` const batchesKey = `/api/sync/batches?projectId=${arg.projectId}&targetSystem=${targetSystem}` globalMutate(statusKey) globalMutate(batchesKey) return result } catch (error) { console.error('Sync trigger failed:', error) throw error } }, [trigger]) return { triggerSync, isLoading: isMutating, error: error as ApiError | null } } // ✅ 동기화 설정 업데이트 (뮤테이션) export function useUpdateSyncConfig() { const { trigger, isMutating, error } = useSWRMutation( '/api/sync/config', async (url: string, { arg }: { arg: Partial & { projectId: number; targetSystem: string } }) => { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(arg) }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) const error = new Error(errorData.message || `설정 업데이트 실패: HTTP ${response.status}`) as ApiError error.status = response.status error.response = errorData throw error } return response.json() } // ✅ onSuccess 콜백 제거 ) // ✅ 수동 캐시 무효화를 포함한 래핑 함수 const updateConfig = React.useCallback(async (arg: Partial & { projectId: number; targetSystem: string }) => { try { const result = await trigger(arg) // ✅ 성공 후 수동으로 캐시 무효화 const { projectId, targetSystem } = arg const configKey = `/api/sync/config?projectId=${projectId}&targetSystem=${targetSystem}` globalMutate(configKey) return result } catch (error) { console.error('Sync config update failed:', error) throw error } }, [trigger]) return { updateConfig, isLoading: isMutating, error: error as ApiError | null } } // ✅ 실시간 동기화 상태 훅 (높은 갱신 빈도) export function useRealtimeSyncStatus(projectId: number | null, targetSystem: string = 'SHI') { const key = projectId ? `/api/sync/status?projectId=${projectId}&targetSystem=${targetSystem}&realtime=true` : null const { data, error, isLoading } = useSWR( key, fetcher, { refreshInterval: 5000, // 5초마다 갱신 revalidateOnFocus: true, revalidateOnReconnect: true, shouldRetryOnError: (error: ApiError) => error.status >= 500 || error.status === 0, dedupingInterval: 2000, // 실시간이므로 중복 제거 간격 단축 } ) const safeData = React.useMemo(() => { return data || createDefaultSyncStatus( error ? (error as ApiError).message : undefined, projectId || undefined ) }, [data, error, projectId]) return { syncStatus: safeData, isLoading: isLoading && Boolean(projectId), error: error as ApiError | null } } // 🔧 유틸리티 함수들 export const syncUtils = { // 캐시 수동 무효화 invalidateCache: (projectId: number, targetSystem: string = 'SHI') => { const statusKey = `/api/sync/status?projectId=${projectId}&targetSystem=${targetSystem}` const batchesKey = `/api/sync/batches?projectId=${projectId}&targetSystem=${targetSystem}` const configKey = `/api/sync/config?projectId=${projectId}&targetSystem=${targetSystem}` globalMutate(statusKey) globalMutate(batchesKey) globalMutate(configKey) }, // 모든 동기화 관련 캐시 무효화 invalidateAllCache: () => { globalMutate((key) => typeof key === 'string' && key.startsWith('/api/sync/')) }, // 에러 메시지 정리 formatError: (error: ApiError | null): string => { if (!error) return '' if (error.status === 0) return '네트워크 연결을 확인해주세요' if (error.status === 401) return '인증이 필요합니다' if (error.status === 403) return '권한이 없습니다' if (error.status === 404) return '요청한 리소스를 찾을 수 없습니다' if (error.status >= 500) return '서버 오류가 발생했습니다' return error.message || '알 수 없는 오류가 발생했습니다' } }