diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-22 02:57:00 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-22 02:57:00 +0000 |
| commit | ee57cc221ff2edafd3c0f12a181214c602ed257e (patch) | |
| tree | 148f552f503798f7a350d6eff936b889f16be49f /hooks/use-sync-status.ts | |
| parent | 14f61e24947fb92dd71ec0a7196a6e815f8e66da (diff) | |
(대표님, 최겸) 이메일 템플릿, 벤더데이터 변경사항 대응, 기술영업 변경요구사항 구현
Diffstat (limited to 'hooks/use-sync-status.ts')
| -rw-r--r-- | hooks/use-sync-status.ts | 437 |
1 files changed, 364 insertions, 73 deletions
diff --git a/hooks/use-sync-status.ts b/hooks/use-sync-status.ts index 07cb3432..99810fdc 100644 --- a/hooks/use-sync-status.ts +++ b/hooks/use-sync-status.ts @@ -1,80 +1,302 @@ -// hooks/use-sync-status.ts (수정된 버전) -import useSWR from 'swr' +// hooks/use-sync-status.ts (완전히 개선된 버전) +import useSWR, { mutate as globalMutate } from 'swr' import useSWRMutation from 'swr/mutation' -import { mutate } from 'swr' - -// 단순한 fetcher 함수들 -const fetcher = async (url: string) => { - const response = await fetch(url) - if (!response.ok) { - const error = new Error(`HTTP ${response.status}`) - ;(error as any).status = response.status +import * as React from 'react' + +// 🔧 타입 정의 강화 +interface SyncStatus { + syncEnabled: boolean + pendingChanges: number + syncedChanges: number + failedChanges: number + lastSyncAt?: string | null + error?: string | null + contractId?: number + targetSystem?: string + lastUpdated?: string +} + +interface ApiError extends Error { + status: number + response?: any +} + +interface SyncBatch { + id: string + contractId: number + targetSystem: string + status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' + startedAt?: string + completedAt?: string + itemCount: number + successCount: number + failureCount: number + errors?: string[] +} + +interface SyncConfig { + contractId: 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<any> => { + 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 } - return response.json() } -// 동기화 상태 조회 -export function useSyncStatus(contractId: number, targetSystem: string = 'SHI') { +// 🔧 안전한 기본값 생성 +const createDefaultSyncStatus = (error?: string, contractId?: number): SyncStatus => ({ + syncEnabled: false, + pendingChanges: 0, + syncedChanges: 0, + failedChanges: 0, + lastSyncAt: null, + error, + contractId, + lastUpdated: new Date().toISOString() +}) + +// ✅ 단일 계약 동기화 상태 조회 +export function useSyncStatus(contractId: number | null, targetSystem: string = 'SHI') { const key = contractId ? `/api/sync/status?contractId=${contractId}&targetSystem=${targetSystem}` : null - const { data, error, isLoading } = useSWR( + const { data, error, isLoading, mutate: localMutate } = useSWR<SyncStatus>( key, fetcher, { - refreshInterval: 30000, // 30초마다 갱신 + refreshInterval: 30000, revalidateOnFocus: true, revalidateOnReconnect: true, - shouldRetryOnError: false, // 에러시 자동 재시도 비활성화 - dedupingInterval: 5000, // 5초 내 중복 요청 방지 + 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 = () => { + const refetch = React.useCallback(() => { if (key) { - mutate(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), + contractId: contractId || data.contractId, + targetSystem: targetSystem, + lastUpdated: new Date().toISOString() + } + } + + return createDefaultSyncStatus( + error ? (error as ApiError).message : undefined, + contractId || undefined + ) + }, [data, error, contractId, targetSystem]) return { - syncStatus: data, - isLoading, - error, + syncStatus: safeData, + isLoading: isLoading && Boolean(contractId), + error: error as ApiError | null, refetch } } -// 동기화 배치 목록 조회 -export function useSyncBatches(contractId: number, targetSystem: string = 'SHI') { +// ❌ useMultipleSyncStatus 제거 (Hook 규칙 위반 때문에) +// 대신 useDynamicSyncStatus 사용 권장 + +// ✅ 다중 계약 동기화 상태 조회 (Hook 규칙 준수) +export function useDynamicSyncStatus(contractIds: number[], targetSystem: string = 'SHI') { + // Hook 규칙 준수: 고정된 수의 Hook 호출 + const paddedContractIds = React.useMemo(() => { + // 입력 검증 및 경고 + if (contractIds.length > MAX_CONTRACTS) { + console.warn(`Contract count (${contractIds.length}) exceeds maximum (${MAX_CONTRACTS}). Only first ${MAX_CONTRACTS} will be processed.`) + } + + // 🔥 명시적 타입 선언 + const padded: (number | null)[] = [...contractIds.slice(0, MAX_CONTRACTS)] + + // null로 패딩 + while (padded.length < MAX_CONTRACTS) { + padded.push(null) + } + + return padded + }, [contractIds]) + // 각 contractId에 대해 고정된 수의 Hook 호출 + const allResults = paddedContractIds.map((contractId) => { + const result = useSyncStatus(contractId, targetSystem) + return contractId ? { contractId, ...result } : null + }) + + // 유효한 결과만 필터링 + const validResults = React.useMemo(() => { + return allResults.filter((result): result is { + contractId: 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 && contractIds.length > 0 + } + }, [validResults, contractIds.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(contractIds: number[], targetSystem: string = 'SHI') { + const [isClient, setIsClient] = React.useState(false) + + React.useEffect(() => { + setIsClient(true) + }, []) + + const syncResult = useDynamicSyncStatus( + isClient ? contractIds : [], + targetSystem + ) + + // 서버 사이드에서는 안전한 기본값 반환 + if (!isClient) { + return { + contractStatuses: [], + totalStats: { + totalPending: 0, + totalSynced: 0, + totalFailed: 0, + hasError: false, + isLoading: true, + canSync: false + }, + refetchAll: () => {} + } + } + + return syncResult +} + +// ✅ 동기화 배치 목록 조회 +export function useSyncBatches(contractId: number | null, targetSystem: string = 'SHI') { const key = contractId ? `/api/sync/batches?contractId=${contractId}&targetSystem=${targetSystem}` : null - const { data, error, isLoading } = useSWR( + const { data, error, isLoading } = useSWR<SyncBatch[]>( key, fetcher, { revalidateOnFocus: false, shouldRetryOnError: false, + refreshInterval: 10000, // 10초마다 갱신 } ) return { - syncBatches: data, - isLoading, - error + syncBatches: data || [], + isLoading: isLoading && Boolean(contractId), + error: error as ApiError | null } } -// 동기화 설정 조회 -export function useSyncConfig(contractId: number, targetSystem: string = 'SHI') { +// ✅ 동기화 설정 조회 +export function useSyncConfig(contractId: number | null, targetSystem: string = 'SHI') { const key = contractId ? `/api/sync/config?contractId=${contractId}&targetSystem=${targetSystem}` : null - const { data, error, isLoading } = useSWR( + const { data, error, isLoading, mutate: localMutate } = useSWR<SyncConfig>( key, fetcher, { @@ -83,14 +305,21 @@ export function useSyncConfig(contractId: number, targetSystem: string = 'SHI') } ) + const refetch = React.useCallback(() => { + if (key) { + localMutate() + } + }, [key, localMutate]) + return { syncConfig: data, - isLoading, - error + isLoading: isLoading && Boolean(contractId), + error: error as ApiError | null, + refetch } } -// 동기화 트리거 (뮤테이션) +// ✅ 동기화 트리거 (뮤테이션) export function useTriggerSync() { const { trigger, isMutating, error } = useSWRMutation( '/api/sync/trigger', @@ -103,41 +332,47 @@ export function useTriggerSync() { if (!response.ok) { const errorData = await response.json().catch(() => ({})) - const error = new Error(errorData.message || `HTTP ${response.status}`) - ;(error as any).status = response.status + const error = new Error(errorData.message || `동기화 실패: HTTP ${response.status}`) as ApiError + error.status = response.status + error.response = errorData throw error } return response.json() - }, - { - onSuccess: (data, key, config) => { - // 관련 캐시 무효화 - const { contractId, targetSystem = 'SHI' } = config.arg - const statusKey = `/api/sync/status?contractId=${contractId}&targetSystem=${targetSystem}` - const batchesKey = `/api/sync/batches?contractId=${contractId}&targetSystem=${targetSystem}` - - mutate(statusKey) - mutate(batchesKey) - }, - onError: (error) => { - console.error('Sync trigger failed:', error) - } } ) + const triggerSync = React.useCallback(async (arg: { contractId: number; targetSystem?: string }) => { + try { + const result = await trigger(arg) + + // 성공 후 관련 캐시 무효화 + const targetSystem = arg.targetSystem || 'SHI' + const statusKey = `/api/sync/status?contractId=${arg.contractId}&targetSystem=${targetSystem}` + const batchesKey = `/api/sync/batches?contractId=${arg.contractId}&targetSystem=${targetSystem}` + + globalMutate(statusKey) + globalMutate(batchesKey) + + return result + } catch (error) { + console.error('Sync trigger failed:', error) + throw error + } + }, [trigger]) + return { - triggerSync: trigger, + triggerSync, isLoading: isMutating, - error + error: error as ApiError | null } } -// 동기화 설정 업데이트 (뮤테이션) +// ✅ 동기화 설정 업데이트 (뮤테이션) export function useUpdateSyncConfig() { const { trigger, isMutating, error } = useSWRMutation( '/api/sync/config', - async (url: string, { arg }: { arg: any }) => { + async (url: string, { arg }: { arg: Partial<SyncConfig> & { contractId: number; targetSystem: string } }) => { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -146,44 +381,100 @@ export function useUpdateSyncConfig() { if (!response.ok) { const errorData = await response.json().catch(() => ({})) - const error = new Error(errorData.message || `HTTP ${response.status}`) - ;(error as any).status = response.status + const error = new Error(errorData.message || `설정 업데이트 실패: HTTP ${response.status}`) as ApiError + error.status = response.status + error.response = errorData throw error } return response.json() - }, - { - onSuccess: (data, key, config) => { - // 설정 캐시 무효화 - const { contractId, targetSystem } = config.arg - const configKey = `/api/sync/config?contractId=${contractId}&targetSystem=${targetSystem}` - mutate(configKey) - }, } + // ✅ onSuccess 콜백 제거 ) + // ✅ 수동 캐시 무효화를 포함한 래핑 함수 + const updateConfig = React.useCallback(async (arg: Partial<SyncConfig> & { contractId: number; targetSystem: string }) => { + try { + const result = await trigger(arg) + + // ✅ 성공 후 수동으로 캐시 무효화 + const { contractId, targetSystem } = arg + const configKey = `/api/sync/config?contractId=${contractId}&targetSystem=${targetSystem}` + globalMutate(configKey) + + return result + } catch (error) { + console.error('Sync config update failed:', error) + throw error + } + }, [trigger]) + return { - updateConfig: trigger, + updateConfig, isLoading: isMutating, - error + error: error as ApiError | null } } - -// 실시간 동기화 상태 훅 (높은 갱신 빈도) -export function useRealtimeSyncStatus(contractId: number, targetSystem: string = 'SHI') { +// ✅ 실시간 동기화 상태 훅 (높은 갱신 빈도) +export function useRealtimeSyncStatus(contractId: number | null, targetSystem: string = 'SHI') { const key = contractId ? `/api/sync/status?contractId=${contractId}&targetSystem=${targetSystem}&realtime=true` : null - return useSWR( + const { data, error, isLoading } = useSWR<SyncStatus>( key, fetcher, { - refreshInterval: 5000, // 5초마다 갱신 (실시간) + refreshInterval: 5000, // 5초마다 갱신 revalidateOnFocus: true, revalidateOnReconnect: true, - shouldRetryOnError: false, + shouldRetryOnError: (error: ApiError) => error.status >= 500 || error.status === 0, + dedupingInterval: 2000, // 실시간이므로 중복 제거 간격 단축 } ) + + const safeData = React.useMemo(() => { + return data || createDefaultSyncStatus( + error ? (error as ApiError).message : undefined, + contractId || undefined + ) + }, [data, error, contractId]) + + return { + syncStatus: safeData, + isLoading: isLoading && Boolean(contractId), + error: error as ApiError | null + } +} + +// 🔧 유틸리티 함수들 +export const syncUtils = { + // 캐시 수동 무효화 + invalidateCache: (contractId: number, targetSystem: string = 'SHI') => { + const statusKey = `/api/sync/status?contractId=${contractId}&targetSystem=${targetSystem}` + const batchesKey = `/api/sync/batches?contractId=${contractId}&targetSystem=${targetSystem}` + const configKey = `/api/sync/config?contractId=${contractId}&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 || '알 수 없는 오류가 발생했습니다' + } }
\ No newline at end of file |
