From 86c00355612da8b956dcf5b1e83036ec2d4bdc55 Mon Sep 17 00:00:00 2001 From: hansoo Date: Fri, 21 Mar 2025 15:45:43 +0900 Subject: [PATCH] =?UTF-8?q?=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=81=AC=20?= =?UTF-8?q?=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0=20=EA=B0=9C=EC=84=A0:=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=84=A0=EC=96=B8=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20=EB=B0=8F=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EA=B4=80=EB=A6=AC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/NetworkStatusIndicator.tsx | 151 +++++++ src/utils/networkUtils.ts | 516 ++++++++++++++++++++++ src/utils/syncUtils.ts | 272 ++++++++++-- 3 files changed, 903 insertions(+), 36 deletions(-) create mode 100644 src/components/NetworkStatusIndicator.tsx create mode 100644 src/utils/networkUtils.ts diff --git a/src/components/NetworkStatusIndicator.tsx b/src/components/NetworkStatusIndicator.tsx new file mode 100644 index 0000000..c7318cc --- /dev/null +++ b/src/components/NetworkStatusIndicator.tsx @@ -0,0 +1,151 @@ +import React, { useEffect, useState } from 'react'; +import { getNetworkStatus, onNetworkStatusChange, NetworkStatus } from '../utils/networkUtils'; +import { getSyncState, onSyncStateChange, SyncState } from '../utils/syncUtils'; +import { toast } from '../hooks/toast'; + +/** + * 네트워크 상태 표시기 컴포넌트 속성 + */ +interface NetworkStatusIndicatorProps { + showToast?: boolean; + showIndicator?: boolean; +} + +/** + * 네트워크 상태 및 동기화 상태를 표시하는 컴포넌트 + */ +const NetworkStatusIndicator: React.FC = ({ + showToast = true, + showIndicator = true +}) => { + // 네트워크 상태 + const [networkStatus, setNetworkStatus] = useState(getNetworkStatus()); + // 동기화 상태 + const [syncState, setSyncState] = useState(getSyncState()); + + useEffect(() => { + // 네트워크 상태 변경 감지 + const unsubscribeNetwork = onNetworkStatusChange((status) => { + setNetworkStatus(status); + + // 오프라인 상태가 되면 토스트 표시 + if (status === 'offline' && showToast) { + toast({ + title: '네트워크 연결 끊김', + description: '네트워크 연결이 끊겼습니다. 오프라인 모드로 전환됩니다.', + variant: 'destructive', + }); + } + + // 온라인 상태로 돌아오면 토스트 표시 + if (status === 'online' && showToast) { + toast({ + title: '네트워크 연결 복구', + description: '네트워크 연결이 복구되었습니다. 데이터 동기화가 재개됩니다.', + variant: 'default', + }); + } + }); + + // 동기화 상태 변경 감지 + const unsubscribeSync = onSyncStateChange((state) => { + setSyncState(state); + + // 동기화 오류가 발생하면 토스트 표시 + if (state.error && showToast) { + toast({ + title: '동기화 오류', + description: `동기화 중 오류가 발생했습니다: ${state.error}`, + variant: 'destructive', + }); + } + }); + + return () => { + // 구독 해제 + unsubscribeNetwork(); + unsubscribeSync(); + }; + }, [showToast]); + + // 네트워크 상태에 따른 스타일 및 메시지 + const getNetworkStatusStyle = () => { + switch (networkStatus) { + case 'online': + return { color: '#4caf50', message: '온라인' }; + case 'offline': + return { color: '#f44336', message: '오프라인' }; + case 'reconnecting': + return { color: '#ff9800', message: '재연결 중' }; + default: + return { color: '#9e9e9e', message: '알 수 없음' }; + } + }; + + // 동기화 상태에 따른 스타일 및 메시지 + const getSyncStatusStyle = () => { + if (syncState.isSyncing) { + return { color: '#2196f3', message: '동기화 중...' }; + } + + if (syncState.error) { + return { color: '#f44336', message: '동기화 오류' }; + } + + if (!syncState.isEnabled) { + return { color: '#9e9e9e', message: '동기화 비활성화' }; + } + + return { color: '#4caf50', message: '동기화됨' }; + }; + + // 인디케이터 스타일 + const networkStyle = getNetworkStatusStyle(); + const syncStyle = getSyncStatusStyle(); + + // 인디케이터 컴포넌트 + const indicatorStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '4px 8px', + borderRadius: '4px', + fontSize: '12px', + fontWeight: 'bold', + margin: '0 4px' + }; + + if (!showIndicator) { + return null; + } + + return ( +
+
+ {networkStyle.message} +
+ +
+ {syncStyle.message} +
+
+ ); +}; + +export default NetworkStatusIndicator; diff --git a/src/utils/networkUtils.ts b/src/utils/networkUtils.ts new file mode 100644 index 0000000..a546125 --- /dev/null +++ b/src/utils/networkUtils.ts @@ -0,0 +1,516 @@ +/** + * 네트워크 상태 관리 및 오류 처리를 위한 유틸리티 + */ + +// 네트워크 상태 저장 키 +const NETWORK_STATUS_KEY = 'network_status'; + +// 네트워크 재시도 설정 +const MAX_RETRY_COUNT = 3; +const INITIAL_RETRY_DELAY = 1000; // 1초 + +// 네트워크 재연결 시도 설정 +const RECONNECT_INTERVAL = 5000; // 5초마다 재연결 시도 +const MAX_RECONNECT_ATTEMPTS = 10; // 최대 재연결 시도 횟수 +let reconnectAttempts = 0; +let reconnectTimer: ReturnType | null = null; + +// 네트워크 모니터링 상태 +let isMonitoring = false; +let networkCheckInterval: ReturnType | null = null; + +// 네트워크 상태 타입 +export type NetworkStatus = 'online' | 'offline' | 'reconnecting'; + +// 동기화 작업 큐 아이템 타입 +export interface SyncQueueItem { + id: string; + type: 'upload' | 'download' | 'delete'; + entityType: 'transaction' | 'budget' | 'categoryBudget'; + data: Record; + timestamp: number; + retryCount: number; +} + +// 네트워크 이벤트 핸들러 인터페이스 +interface NetworkEventHandlers { + handleOnline: () => void; + handleOffline: () => void; +} + +// 확장된 Window 인터페이스 +interface ExtendedWindow extends Window { + __networkHandlers?: NetworkEventHandlers; +} + +/** + * 현재 네트워크 상태 확인 + */ +export const checkNetworkStatus = async (): Promise => { + try { + // 간단한 네트워크 연결 테스트 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5초 타임아웃 + + const response = await fetch('https://www.google.com', { + method: 'HEAD', + signal: controller.signal + }); + + clearTimeout(timeoutId); + + const isOnline = response.ok; + setNetworkStatus(isOnline ? 'online' : 'offline'); + return isOnline; + } catch (error) { + console.error('[네트워크] 연결 확인 실패:', error); + setNetworkStatus('offline'); + return false; + } +}; + +/** + * 네트워크 상태 저장 + */ +export const setNetworkStatus = (status: NetworkStatus): void => { + try { + localStorage.setItem(NETWORK_STATUS_KEY, status); + // 상태 변경 이벤트 발생 + window.dispatchEvent(new CustomEvent('networkStatusChange', { detail: status })); + console.log(`[네트워크] 상태 변경: ${status}`); + } catch (error) { + console.error('[네트워크] 상태 저장 실패:', error); + } +}; + +/** + * 저장된 네트워크 상태 가져오기 + */ +export const getNetworkStatus = (): NetworkStatus => { + try { + const status = localStorage.getItem(NETWORK_STATUS_KEY) as NetworkStatus; + return status || 'online'; // 기본값은 온라인으로 설정 + } catch (error) { + console.error('[네트워크] 상태 조회 실패:', error); + return 'online'; // 오류 시 기본값 + } +}; + +/** + * 네트워크 상태 모니터링 시작 + */ +export const startNetworkMonitoring = (): void => { + if (isMonitoring) { + return; + } + + isMonitoring = true; + + // 초기 네트워크 상태 확인 + checkNetworkStatus().then(isOnline => { + setNetworkStatus(isOnline ? 'online' : 'offline'); + + if (!isOnline) { + // 오프라인 상태면 재연결 시도 시작 + reconnectTimer = setTimeout(attemptReconnect, RECONNECT_INTERVAL); + } + }); + + // 브라우저 온라인/오프라인 이벤트 리스너 + const handleOnline = () => { + console.log('[네트워크] 브라우저 온라인 이벤트 감지'); + setNetworkStatus('online'); + reconnectAttempts = 0; + + // 대기 중인 동기화 작업 처리 + processPendingSyncQueue().catch(err => { + console.error('[네트워크] 온라인 전환 후 동기화 큐 처리 중 오류:', err); + }); + }; + + const handleOffline = () => { + console.log('[네트워크] 브라우저 오프라인 이벤트 감지'); + setNetworkStatus('offline'); + + // 재연결 시도 시작 + reconnectTimer = setTimeout(attemptReconnect, RECONNECT_INTERVAL); + }; + + // 이벤트 리스너 등록 및 참조 저장 + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + + // 전역 변수에 이벤트 핸들러 참조 저장 + (window as ExtendedWindow).__networkHandlers = { + handleOnline, + handleOffline + }; + + // 정기적인 네트워크 상태 확인 (30초마다) + networkCheckInterval = setInterval(async () => { + try { + const isOnline = await checkNetworkStatus(); + const currentStatus = getNetworkStatus(); + + if (isOnline && currentStatus !== 'online') { + // 오프라인에서 온라인으로 변경 + setNetworkStatus('online'); + reconnectAttempts = 0; + + // 대기 중인 동기화 작업 처리 + processPendingSyncQueue().catch(err => { + console.error('[네트워크] 정기 확인 후 동기화 큐 처리 중 오류:', err); + }); + } else if (!isOnline && currentStatus === 'online') { + // 온라인에서 오프라인으로 변경 + setNetworkStatus('offline'); + + // 재연결 시도 시작 + if (!reconnectTimer) { + reconnectTimer = setTimeout(attemptReconnect, RECONNECT_INTERVAL); + } + } + } catch (error) { + console.error('[네트워크] 정기 네트워크 상태 확인 중 오류:', error); + } + }, 30000); + + console.log('[네트워크] 네트워크 모니터링 시작'); +}; + +/** + * 네트워크 상태 모니터링 중지 + */ +export const stopNetworkMonitoring = (): void => { + if (!isMonitoring) { + return; + } + + isMonitoring = false; + + // 브라우저 이벤트 리스너 제거 + const handlers = (window as ExtendedWindow).__networkHandlers; + if (handlers) { + window.removeEventListener('online', handlers.handleOnline); + window.removeEventListener('offline', handlers.handleOffline); + delete (window as ExtendedWindow).__networkHandlers; + } + + // 인터벌 및 타이머 정리 + if (networkCheckInterval) { + clearInterval(networkCheckInterval); + networkCheckInterval = null; + } + + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + reconnectAttempts = 0; + + console.log('[네트워크] 네트워크 모니터링 중지'); +}; + +/** + * 네트워크 재연결 시도 함수 + */ +export const attemptReconnect = async (): Promise => { + if (getNetworkStatus() === 'online') { + // 이미 온라인 상태면 재연결 필요 없음 + reconnectAttempts = 0; + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + return true; + } + + // 재연결 시도 중 상태로 변경 + setNetworkStatus('reconnecting'); + + try { + const isOnline = await checkNetworkStatus(); + if (isOnline) { + // 재연결 성공 + setNetworkStatus('online'); + reconnectAttempts = 0; + console.log('[네트워크] 재연결 성공'); + + // 대기 중인 동기화 작업 처리 + processPendingSyncQueue().catch(err => { + console.error('[네트워크] 재연결 후 동기화 큐 처리 중 오류:', err); + }); + + return true; + } else { + // 재연결 실패 + reconnectAttempts++; + console.log(`[네트워크] 재연결 실패 (시도 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`); + + if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + // 최대 재연결 시도 횟수 초과 + setNetworkStatus('offline'); + reconnectAttempts = 0; + console.log('[네트워크] 최대 재연결 시도 횟수 초과, 오프라인 모드로 전환'); + return false; + } + + // 다음 재연결 시도 예약 + reconnectTimer = setTimeout(attemptReconnect, RECONNECT_INTERVAL); + return false; + } + } catch (error) { + console.error('[네트워크] 재연결 시도 중 오류:', error); + setNetworkStatus('offline'); + reconnectAttempts = 0; + return false; + } +}; + +/** + * 온라인 상태 처리 + */ +const handleOnline = async () => { + console.log('[네트워크] 온라인 상태 감지'); + setNetworkStatus('reconnecting'); + + // 실제 연결 확인 (브라우저 이벤트는 때때로 부정확할 수 있음) + const isReallyOnline = await checkNetworkStatus(); + + if (isReallyOnline) { + console.log('[네트워크] 온라인 상태 확인됨, 보류 중인 작업 처리 시작'); + processPendingSyncQueue(); + } +}; + +/** + * 오프라인 상태 처리 + */ +const handleOffline = () => { + console.log('[네트워크] 오프라인 상태 감지'); + setNetworkStatus('offline'); +}; + +// 동기화 작업 큐 관리 +const SYNC_QUEUE_KEY = 'sync_queue'; + +/** + * 동기화 작업을 큐에 추가 + */ +export const addToSyncQueue = (operation: { + type: 'upload' | 'download' | 'delete'; + entityType: 'transaction' | 'budget' | 'categoryBudget'; + data: Record; + timestamp?: number; + retries?: number; +}): void => { + // 타임스탬프 및 재시도 횟수 추가 + const enhancedOperation = { + ...operation, + timestamp: operation.timestamp || Date.now(), + retries: operation.retries || 0 + }; + + // 로컬 스토리지에서 기존 큐 가져오기 + const queueString = localStorage.getItem(SYNC_QUEUE_KEY) || '[]'; + let queue: typeof enhancedOperation[] = []; + + try { + queue = JSON.parse(queueString); + } catch (error) { + console.error('[네트워크] 동기화 큐 파싱 오류:', error); + queue = []; + } + + // 큐에 작업 추가 + queue.push(enhancedOperation); + + // 큐 저장 + localStorage.setItem(SYNC_QUEUE_KEY, JSON.stringify(queue)); + + console.log(`[네트워크] 동기화 큐에 작업 추가: ${operation.type} ${operation.entityType}`); +}; + +/** + * 동기화 큐 가져오기 + */ +export const getSyncQueue = (): SyncQueueItem[] => { + try { + const queueStr = localStorage.getItem(SYNC_QUEUE_KEY); + return queueStr ? JSON.parse(queueStr) : []; + } catch (error) { + console.error('[네트워크] 동기화 큐 조회 실패:', error); + return []; + } +}; + +/** + * 동기화 큐에서 항목 제거 + */ +export const removeFromSyncQueue = (itemId: string): void => { + try { + const queue = getSyncQueue(); + const updatedQueue = queue.filter(item => item.id !== itemId); + localStorage.setItem(SYNC_QUEUE_KEY, JSON.stringify(updatedQueue)); + console.log(`[네트워크] 동기화 큐에서 작업 제거: ${itemId}`); + } catch (error) { + console.error('[네트워크] 동기화 큐 항목 제거 실패:', error); + } +}; + +/** + * 동기화 큐 항목 업데이트 + */ +export const updateSyncQueueItem = (itemId: string, updates: Partial): void => { + try { + const queue = getSyncQueue(); + const updatedQueue = queue.map(item => + item.id === itemId ? { ...item, ...updates } : item + ); + localStorage.setItem(SYNC_QUEUE_KEY, JSON.stringify(updatedQueue)); + } catch (error) { + console.error('[네트워크] 동기화 큐 항목 업데이트 실패:', error); + } +}; + +/** + * 보류 중인 동기화 작업 처리 + */ +export const processPendingSyncQueue = async (): Promise => { + if (getNetworkStatus() !== 'online') { + console.log('[네트워크] 오프라인 상태에서 동기화 큐 처리 불가'); + return; + } + + const queue = getSyncQueue(); + if (queue.length === 0) { + console.log('[네트워크] 처리할 동기화 작업 없음'); + return; + } + + console.log(`[네트워크] ${queue.length}개의 보류 중인 동기화 작업 처리 시작`); + + // 큐의 각 항목을 순차적으로 처리 + for (const item of queue) { + try { + // 여기서 실제 동기화 작업 수행 (구현 필요) + // 예: await processSyncItem(item); + + // 성공 시 큐에서 제거 + removeFromSyncQueue(item.id); + console.log(`[네트워크] 동기화 작업 성공: ${item.id}`); + } catch (error) { + console.error(`[네트워크] 동기화 작업 실패: ${item.id}`, error); + + // 재시도 횟수 증가 + const newRetryCount = item.retryCount + 1; + + if (newRetryCount <= MAX_RETRY_COUNT) { + // 지수 백오프 알고리즘 적용 (재시도 간격을 점점 늘림) + const retryDelay = INITIAL_RETRY_DELAY * Math.pow(2, newRetryCount - 1); + console.log(`[네트워크] ${retryDelay}ms 후 재시도 예정 (${newRetryCount}/${MAX_RETRY_COUNT})`); + + // 재시도 횟수 업데이트 + updateSyncQueueItem(item.id, { retryCount: newRetryCount }); + } else { + // 최대 재시도 횟수 초과 시 실패로 처리하고 큐에서 제거 + console.error(`[네트워크] 최대 재시도 횟수 초과, 작업 실패: ${item.id}`); + removeFromSyncQueue(item.id); + + // 사용자에게 알림 (구현 필요) + // 예: notifyUser(`동기화 작업 실패: ${item.entityType}`); + } + } + } +}; + +/** + * 네트워크 요청 래퍼 함수 (재시도 로직 포함) + */ +export const withRetry = async ( + fn: () => Promise, + options: { + maxRetries?: number; + retryDelay?: number; + onRetry?: (attempt: number, error: Error | unknown) => void; + shouldRetry?: (error: Error | unknown) => boolean; + } = {} +): Promise => { + const { + maxRetries = 3, + retryDelay = 1000, + onRetry = (attempt, error) => console.log(`[네트워크] 재시도 중 (${attempt}/${maxRetries}):`, error), + shouldRetry = (error) => true // 기본적으로 모든 오류에 대해 재시도 + } = options; + + let lastError: Error | unknown; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + // 네트워크 상태 확인 + if (attempt > 0 && getNetworkStatus() !== 'online') { + throw new Error('오프라인 상태입니다.'); + } + + return await fn(); + } catch (error) { + lastError = error; + + // 마지막 시도였거나 재시도하지 않아야 하는 오류인 경우 + if (attempt === maxRetries || !shouldRetry(error)) { + throw error; + } + + // 재시도 콜백 호출 + onRetry(attempt + 1, error); + + // 재시도 전 지연 + await new Promise(resolve => setTimeout(resolve, retryDelay * (attempt + 1))); + } + } + + // 이 코드는 실행되지 않아야 하지만, TypeScript 컴파일러를 위해 추가 + throw lastError; +}; + +/** + * 네트워크 요청 타임아웃 래퍼 함수 + */ +export const withTimeout = ( + promise: Promise, + timeoutMs: number = 10000 +): Promise => { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error(`요청 시간 초과 (${timeoutMs}ms)`)); + }, timeoutMs); + + promise + .then(result => { + clearTimeout(timeoutId); + resolve(result); + }) + .catch(error => { + clearTimeout(timeoutId); + reject(error); + }); + }); +}; + +/** + * 네트워크 상태 변경 이벤트 리스너 등록 + */ +export const onNetworkStatusChange = (callback: (status: NetworkStatus) => void): () => void => { + const handler = (event: Event) => { + const customEvent = event as CustomEvent; + callback(customEvent.detail); + }; + + window.addEventListener('networkStatusChange', handler); + + // 구독 해제 함수 반환 + return () => { + window.removeEventListener('networkStatusChange', handler); + }; +}; diff --git a/src/utils/syncUtils.ts b/src/utils/syncUtils.ts index 6d1c5f7..5838b9b 100644 --- a/src/utils/syncUtils.ts +++ b/src/utils/syncUtils.ts @@ -1,8 +1,19 @@ - import { isSyncEnabled, setSyncEnabled, getLastSyncTime, setLastSyncTime, initSyncSettings } from './sync/syncSettings'; import { uploadTransactions, downloadTransactions, deleteTransactionFromServer } from './sync/transactionSync'; import { uploadBudgets, downloadBudgets } from './sync/budget'; import { clearCloudData } from './sync/clearCloudData'; +import { + checkNetworkStatus, + getNetworkStatus, + setNetworkStatus, + startNetworkMonitoring, + stopNetworkMonitoring, + withRetry, + addToSyncQueue, + processPendingSyncQueue, + onNetworkStatusChange, + NetworkStatus +} from './networkUtils'; // Export all utility functions to maintain the same public API export { @@ -16,7 +27,96 @@ export { getLastSyncTime, setLastSyncTime, initSyncSettings, - clearCloudData + clearCloudData, + // 네트워크 관련 함수 추가 내보내기 + checkNetworkStatus, + getNetworkStatus, + startNetworkMonitoring, + stopNetworkMonitoring, + onNetworkStatusChange +}; + +/** + * 동기화 상태 인터페이스 + */ +export interface SyncState { + isEnabled: boolean; + lastSyncTime: string | null; + networkStatus: NetworkStatus; + isSyncing: boolean; + error: string | null; +} + +// 현재 동기화 상태 +let syncState: SyncState = { + isEnabled: false, + lastSyncTime: null, + networkStatus: 'online', + isSyncing: false, + error: null +}; + +/** + * 동기화 상태 초기화 + */ +export const initSyncState = async (): Promise => { + syncState = { + isEnabled: isSyncEnabled(), + lastSyncTime: getLastSyncTime(), + networkStatus: getNetworkStatus(), + isSyncing: false, + error: null + }; + + // 네트워크 모니터링 시작 + startNetworkMonitoring(); + + // 네트워크 상태 변경 리스너 등록 + onNetworkStatusChange((status) => { + syncState.networkStatus = status; + // 상태 변경 이벤트 발생 + window.dispatchEvent(new CustomEvent('syncStateChange', { detail: { ...syncState } })); + + // 온라인 상태로 변경되면 보류 중인 동기화 작업 처리 + if (status === 'online') { + processPendingSyncQueue(); + } + }); + + console.log('[동기화] 상태 초기화 완료', syncState); +}; + +/** + * 현재 동기화 상태 가져오기 + */ +export const getSyncState = (): SyncState => { + return { ...syncState }; +}; + +/** + * 동기화 상태 업데이트 + */ +const updateSyncState = (updates: Partial): void => { + syncState = { ...syncState, ...updates }; + // 상태 변경 이벤트 발생 + window.dispatchEvent(new CustomEvent('syncStateChange', { detail: { ...syncState } })); +}; + +/** + * 동기화 상태 변경 이벤트 리스너 등록 + */ +export const onSyncStateChange = (callback: (state: SyncState) => void): () => void => { + const handler = (event: Event) => { + const customEvent = event as CustomEvent; + callback(customEvent.detail); + }; + + window.addEventListener('syncStateChange', handler); + + // 구독 해제 함수 반환 + return () => { + window.removeEventListener('syncStateChange', handler); + }; }; /** @@ -25,64 +125,123 @@ export { export const syncAllData = async (userId: string): Promise => { if (!userId || !isSyncEnabled()) return; + // 네트워크 상태 확인 + const isOnline = await checkNetworkStatus(); + if (!isOnline) { + const error = new Error('오프라인 상태에서 동기화할 수 없습니다.'); + console.error('[동기화] 오류:', error); + updateSyncState({ error: error.message }); + throw error; + } + + // 동기화 상태 업데이트 + updateSyncState({ isSyncing: true, error: null }); + try { - console.log('데이터 동기화 시작...'); + console.log('[동기화] 데이터 동기화 시작...'); // 기존 동기화 순서: 서버에서 먼저 다운로드 후, 로컬 데이터 업로드 // 이 순서를 유지하여 서버에 저장된 데이터를 먼저 가져온 후, 로컬 변경사항을 반영 // 1. 서버에서 데이터 다운로드 (기존 데이터 불러오기) - await downloadTransactions(userId); - await downloadBudgets(userId); + await withRetry( + () => downloadTransactions(userId), + { entityType: '트랜잭션 다운로드' } + ); + + await withRetry( + () => downloadBudgets(userId), + { entityType: '예산 다운로드' } + ); // 약간의 딜레이를 추가하여 다운로드된 데이터가 처리될 시간을 줌 await new Promise(resolve => setTimeout(resolve, 500)); // 2. 로컬 데이터를 서버에 업로드 (변경사항 반영) - await uploadTransactions(userId); - await uploadBudgets(userId); + await withRetry( + () => uploadTransactions(userId), + { entityType: '트랜잭션 업로드' } + ); + + await withRetry( + () => uploadBudgets(userId), + { entityType: '예산 업로드' } + ); // 동기화 시간 업데이트 setLastSyncTime(); - console.log('데이터 동기화 완료!'); + updateSyncState({ + lastSyncTime: getLastSyncTime(), + isSyncing: false + }); + + console.log('[동기화] 데이터 동기화 완료!'); } catch (error) { - console.error('동기화 중 오류 발생:', error); + console.error('[동기화] 오류 발생:', error); + updateSyncState({ + isSyncing: false, + error: error instanceof Error ? error.message : '알 수 없는 오류' + }); throw error; // 오류를 상위 호출자에게 전달하여 적절히 처리하도록 함 } }; -// trySyncAllData의 반환 타입 명시적 정의 // 동기화 결과를 위한 인터페이스 정의 export interface SyncResult { success: boolean; partial?: boolean; downloadSuccess?: boolean; uploadSuccess?: boolean; - error?: any; + error?: Error | string | unknown; } // 안전하게 동기화 시도하는 함수 개선 export const trySyncAllData = async (userId: string): Promise => { if (!userId || !isSyncEnabled()) return { success: true }; + // 네트워크 상태 확인 + const isOnline = await checkNetworkStatus(); + if (!isOnline) { + const errorMsg = '오프라인 상태에서 동기화할 수 없습니다.'; + console.log('[동기화] 경고:', errorMsg); + updateSyncState({ error: errorMsg }); + return { + success: false, + error: errorMsg + }; + } + + // 동기화 상태 업데이트 + updateSyncState({ isSyncing: true, error: null }); + let downloadSuccess = false; let uploadSuccess = false; + let error = null; try { - console.log('안전한 데이터 동기화 시도...'); + console.log('[동기화] 안전한 데이터 동기화 시도...'); try { // 1단계: 서버에서 데이터 다운로드 - await downloadTransactions(userId); - await downloadBudgets(userId); + await withRetry( + () => downloadTransactions(userId), + { entityType: '트랜잭션 다운로드', maxRetries: 2 } + ); - console.log('서버 데이터 다운로드 성공'); + await withRetry( + () => downloadBudgets(userId), + { entityType: '예산 다운로드', maxRetries: 2 } + ); + + console.log('[동기화] 서버 데이터 다운로드 성공'); downloadSuccess = true; // 다운로드 단계가 성공적으로 완료되면 부분 동기화 마킹 setLastSyncTime('부분-다운로드'); + updateSyncState({ lastSyncTime: getLastSyncTime() }); } catch (downloadError) { - console.error('다운로드 동기화 오류:', downloadError); + console.error('[동기화] 다운로드 오류:', downloadError); + error = downloadError; // 다운로드 실패해도 업로드는 시도 - 부분 동기화 } @@ -91,36 +250,77 @@ export const trySyncAllData = async (userId: string): Promise => { try { // 2단계: 로컬 데이터를 서버에 업로드 - await uploadTransactions(userId); - await uploadBudgets(userId); + await withRetry( + () => uploadTransactions(userId), + { entityType: '트랜잭션 업로드', maxRetries: 2 } + ); - console.log('로컬 데이터 업로드 성공'); + await withRetry( + () => uploadBudgets(userId), + { entityType: '예산 업로드', maxRetries: 2 } + ); + + console.log('[동기화] 로컬 데이터 업로드 성공'); uploadSuccess = true; // 업로드까지 성공적으로 완료되면 동기화 시간 업데이트 setLastSyncTime(); + updateSyncState({ lastSyncTime: getLastSyncTime() }); } catch (uploadError) { - console.error('업로드 동기화 오류:', uploadError); - // 업로드 실패해도 다운로드는 성공했을 수 있으므로 부분 성공으로 간주 + console.error('[동기화] 업로드 오류:', uploadError); + if (!error) error = uploadError; // 다운로드에서 오류가 없었을 경우에만 설정 } - // 하나라도 성공했으면 부분 성공으로 간주 - const result: SyncResult = { - success: downloadSuccess || uploadSuccess, - partial: (downloadSuccess && !uploadSuccess) || (!downloadSuccess && uploadSuccess), - downloadSuccess, - uploadSuccess - }; + // 동기화 상태 업데이트 + updateSyncState({ isSyncing: false }); - console.log('동기화 결과:', result); - return result; - } catch (error) { - console.error('전체 동기화 시도 중 오류:', error); - return { - success: false, - error, + // 결과 반환 + const success = downloadSuccess && uploadSuccess; + const partial = (downloadSuccess || uploadSuccess) && !(downloadSuccess && uploadSuccess); + + if (!success && error) { + updateSyncState({ + error: error instanceof Error ? error.message : '알 수 없는 오류' + }); + } + + return { + success, + partial, downloadSuccess, - uploadSuccess + uploadSuccess, + error: error ? (error instanceof Error ? error.message : error) : undefined + }; + } catch (generalError) { + console.error('[동기화] 일반 오류:', generalError); + updateSyncState({ + isSyncing: false, + error: generalError instanceof Error ? generalError.message : '알 수 없는 오류' + }); + + return { + success: false, + error: generalError }; } }; + +/** + * 오프라인 모드에서 작업 큐에 추가하는 함수 + */ +export const queueSyncOperation = ( + type: 'upload' | 'download' | 'delete', + entityType: 'transaction' | 'budget' | 'categoryBudget', + data: Record +): void => { + addToSyncQueue({ type, entityType, data }); + console.log(`[동기화] 작업 큐에 추가: ${type} ${entityType}`); +}; + +/** + * 동기화 시스템 종료 + */ +export const shutdownSyncSystem = (): void => { + stopNetworkMonitoring(); + console.log('[동기화] 시스템 종료'); +};