Files
zellyy-finance/src/utils/networkUtils.ts
2025-03-21 16:32:36 +09:00

520 lines
15 KiB
TypeScript

/**
* 네트워크 상태 관리 및 오류 처리를 위한 유틸리티
*/
// 네트워크 상태 저장 키
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;
}
// 메모리에 네트워크 상태 저장
let networkStatus: NetworkStatus | null = null;
/**
* 현재 네트워크 상태 확인 (개선 버전)
*/
export const checkNetworkStatus = async (): Promise<boolean> => {
try {
// 더 빠른 타임아웃 설정 (2초)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
// 여러 엔드포인트로 확인하여 신뢰성 향상
const endpoints = [
'https://www.google.com',
'https://www.cloudflare.com',
'https://www.apple.com'
];
// 첫 번째 성공 응답 시 온라인으로 판단
const isOnline = await Promise.any(
endpoints.map(url =>
fetch(url, { method: 'HEAD', signal: controller.signal })
.then(res => res.ok)
)
);
clearTimeout(timeoutId);
setNetworkStatus(isOnline ? 'online' : 'offline');
return isOnline;
} catch (error) {
console.error('[네트워크] 연결 확인 실패:', error);
setNetworkStatus('offline');
return false;
}
};
/**
* 네트워크 상태 저장 (메모리 기반)
*/
export const setNetworkStatus = (status: NetworkStatus): void => {
// 메모리에 상태 저장
networkStatus = status;
// 상태 변경 이벤트 발생
window.dispatchEvent(new CustomEvent('networkStatusChange', { detail: status }));
console.log(`[네트워크] 상태 변경: ${status}`);
};
/**
* 네트워크 상태 가져오기 (메모리 기반)
*/
export const getNetworkStatus = (): NetworkStatus => {
return networkStatus || '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);
};
};