summaryrefslogtreecommitdiff
path: root/hooks/use-sync-status.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-22 02:57:00 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-22 02:57:00 +0000
commitee57cc221ff2edafd3c0f12a181214c602ed257e (patch)
tree148f552f503798f7a350d6eff936b889f16be49f /hooks/use-sync-status.ts
parent14f61e24947fb92dd71ec0a7196a6e815f8e66da (diff)
(대표님, 최겸) 이메일 템플릿, 벤더데이터 변경사항 대응, 기술영업 변경요구사항 구현
Diffstat (limited to 'hooks/use-sync-status.ts')
-rw-r--r--hooks/use-sync-status.ts437
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