네트워크 유틸리티 개선: 변수 선언 문제 해결 및 이벤트 핸들러 관리 개선
This commit is contained in:
151
src/components/NetworkStatusIndicator.tsx
Normal file
151
src/components/NetworkStatusIndicator.tsx
Normal file
@@ -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<NetworkStatusIndicatorProps> = ({
|
||||
showToast = true,
|
||||
showIndicator = true
|
||||
}) => {
|
||||
// 네트워크 상태
|
||||
const [networkStatus, setNetworkStatus] = useState<NetworkStatus>(getNetworkStatus());
|
||||
// 동기화 상태
|
||||
const [syncState, setSyncState] = useState<SyncState>(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 (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: '10px',
|
||||
right: '10px',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '5px'
|
||||
}}>
|
||||
<div style={{
|
||||
...indicatorStyle,
|
||||
backgroundColor: networkStyle.color,
|
||||
color: 'white'
|
||||
}}>
|
||||
{networkStyle.message}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
...indicatorStyle,
|
||||
backgroundColor: syncStyle.color,
|
||||
color: 'white'
|
||||
}}>
|
||||
{syncStyle.message}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkStatusIndicator;
|
||||
516
src/utils/networkUtils.ts
Normal file
516
src/utils/networkUtils.ts
Normal file
@@ -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<typeof setTimeout> | null = null;
|
||||
|
||||
// 네트워크 모니터링 상태
|
||||
let isMonitoring = false;
|
||||
let networkCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// 네트워크 상태 타입
|
||||
export type NetworkStatus = 'online' | 'offline' | 'reconnecting';
|
||||
|
||||
// 동기화 작업 큐 아이템 타입
|
||||
export interface SyncQueueItem {
|
||||
id: string;
|
||||
type: 'upload' | 'download' | 'delete';
|
||||
entityType: 'transaction' | 'budget' | 'categoryBudget';
|
||||
data: Record<string, unknown>;
|
||||
timestamp: number;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
// 네트워크 이벤트 핸들러 인터페이스
|
||||
interface NetworkEventHandlers {
|
||||
handleOnline: () => void;
|
||||
handleOffline: () => void;
|
||||
}
|
||||
|
||||
// 확장된 Window 인터페이스
|
||||
interface ExtendedWindow extends Window {
|
||||
__networkHandlers?: NetworkEventHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 네트워크 상태 확인
|
||||
*/
|
||||
export const checkNetworkStatus = async (): Promise<boolean> => {
|
||||
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<boolean> => {
|
||||
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<string, unknown>;
|
||||
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<SyncQueueItem>): 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<void> => {
|
||||
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 <T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
maxRetries?: number;
|
||||
retryDelay?: number;
|
||||
onRetry?: (attempt: number, error: Error | unknown) => void;
|
||||
shouldRetry?: (error: Error | unknown) => boolean;
|
||||
} = {}
|
||||
): Promise<T> => {
|
||||
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 = <T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number = 10000
|
||||
): Promise<T> => {
|
||||
return new Promise<T>((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<NetworkStatus>;
|
||||
callback(customEvent.detail);
|
||||
};
|
||||
|
||||
window.addEventListener('networkStatusChange', handler);
|
||||
|
||||
// 구독 해제 함수 반환
|
||||
return () => {
|
||||
window.removeEventListener('networkStatusChange', handler);
|
||||
};
|
||||
};
|
||||
@@ -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<void> => {
|
||||
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<SyncState>): 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<SyncState>;
|
||||
callback(customEvent.detail);
|
||||
};
|
||||
|
||||
window.addEventListener('syncStateChange', handler);
|
||||
|
||||
// 구독 해제 함수 반환
|
||||
return () => {
|
||||
window.removeEventListener('syncStateChange', handler);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -25,64 +125,123 @@ export {
|
||||
export const syncAllData = async (userId: string): Promise<void> => {
|
||||
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<SyncResult> => {
|
||||
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<SyncResult> => {
|
||||
|
||||
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<string, unknown>
|
||||
): void => {
|
||||
addToSyncQueue({ type, entityType, data });
|
||||
console.log(`[동기화] 작업 큐에 추가: ${type} ${entityType}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 동기화 시스템 종료
|
||||
*/
|
||||
export const shutdownSyncSystem = (): void => {
|
||||
stopNetworkMonitoring();
|
||||
console.log('[동기화] 시스템 종료');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user