From e633eb95d3a0c4c423bcec1b136778ead88342d8 Mon Sep 17 00:00:00 2001 From: hansoo Date: Fri, 21 Mar 2025 17:35:26 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SyncState 인터페이스 개선 및 타입 오류 해결 - withRetry 함수를 네트워크 유틸리티에서 재사용하여 코드 중복 제거 - 오류 처리 및 로깅 개선 - 트랜잭션 다운로드/업로드 로직 최적화 --- src/utils/sync/transaction/dateUtils.ts | 58 +++- .../sync/transaction/downloadTransaction.ts | 135 +++++--- .../sync/transaction/uploadTransaction.ts | 119 ++++--- src/utils/syncUtils.ts | 324 ++++++++---------- 4 files changed, 351 insertions(+), 285 deletions(-) diff --git a/src/utils/sync/transaction/dateUtils.ts b/src/utils/sync/transaction/dateUtils.ts index 701dda7..68d0cf3 100644 --- a/src/utils/sync/transaction/dateUtils.ts +++ b/src/utils/sync/transaction/dateUtils.ts @@ -1,11 +1,17 @@ - -import { formatISO, parseISO, isValid } from 'date-fns'; +import { formatISO, parseISO, isValid, format } from 'date-fns'; +import { ko } from 'date-fns/locale'; /** * 날짜 문자열을 ISO 형식으로 변환하는 함수 - * "오늘, 19:00 PM"과 같은 형식을 처리하기 위한 함수 + * 다양한 형식의 날짜 문자열을 처리 */ export const normalizeDate = (dateStr: string): string => { + // 입력값이 없거나 유효하지 않은 경우 보호 + if (!dateStr || typeof dateStr !== 'string') { + console.warn('[날짜 변환] 유효하지 않은 입력:', dateStr); + return formatISO(new Date()); + } + // 이미 ISO 형식인 경우 그대로 반환 if (dateStr.match(/^\d{4}-\d{2}-\d{2}T/)) { return dateStr; @@ -27,17 +33,38 @@ export const normalizeDate = (dateStr: string): string => { return formatISO(today); } + // 한국어 날짜 형식 처리 (YYYY년 MM월 DD일) + const koreanDateMatch = dateStr.match(/(\d{4})년\s*(\d{1,2})월\s*(\d{1,2})일/); + if (koreanDateMatch) { + const year = parseInt(koreanDateMatch[1], 10); + const month = parseInt(koreanDateMatch[2], 10) - 1; // 월은 0-11 + const day = parseInt(koreanDateMatch[3], 10); + + // 시간 추출 시도 + let hours = 0, minutes = 0; + const timeMatch = dateStr.match(/(\d{1,2}):(\d{2})/); + if (timeMatch) { + hours = parseInt(timeMatch[1], 10); + minutes = parseInt(timeMatch[2], 10); + } + + const date = new Date(year, month, day, hours, minutes); + if (isValid(date)) { + return formatISO(date); + } + } + // 일반 날짜 문자열은 그대로 Date 객체로 변환 시도 const date = new Date(dateStr); - if (isValid(date)) { + if (isValid(date) && !isNaN(date.getTime())) { return formatISO(date); } // 변환 실패 시 현재 시간 반환 - console.warn(`날짜 변환 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`); + console.warn(`[날짜 변환] 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`); return formatISO(new Date()); } catch (error) { - console.error(`날짜 변환 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`, error); + console.error(`[날짜 변환] 심각한 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`, error); // 오류 발생 시 현재 시간 반환 (데이터 손실 방지) return formatISO(new Date()); } @@ -49,18 +76,18 @@ export const normalizeDate = (dateStr: string): string => { export const formatDateForDisplay = (isoDateStr: string): string => { // 입력값이 유효한지 보호 처리 if (!isoDateStr || typeof isoDateStr !== 'string') { - console.warn('유효하지 않은 날짜 입력:', isoDateStr); + console.warn('[날짜 표시] 유효하지 않은 날짜 입력:', isoDateStr); return '날짜 없음'; } try { // 이미 포맷된 날짜 문자열(예: "오늘, 14:30")이면 그대로 반환 if (isoDateStr.includes('오늘,') || - isoDateStr.includes('년') && isoDateStr.includes('월') && isoDateStr.includes('일')) { + (isoDateStr.includes('년') && isoDateStr.includes('월') && isoDateStr.includes('일'))) { return isoDateStr; } - // 유효한 ISO 날짜인지 확인 + // 유효한 날짜 객체 생성 let date; if (isoDateStr.match(/^\d{4}-\d{2}-\d{2}T/)) { // ISO 형식인 경우 @@ -71,7 +98,7 @@ export const formatDateForDisplay = (isoDateStr: string): string => { } if (!isValid(date) || isNaN(date.getTime())) { - console.warn('유효하지 않은 날짜 형식:', isoDateStr); + console.warn('[날짜 표시] 유효하지 않은 날짜 형식:', isoDateStr); return '유효하지 않은 날짜'; } @@ -86,10 +113,15 @@ export const formatDateForDisplay = (isoDateStr: string): string => { return `오늘, ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; } - // 그 외의 경우 YYYY년 MM월 DD일 형식으로 반환 - return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; + // date-fns를 사용하여 한국어 형식으로 변환 + try { + return format(date, 'yyyy년 M월 d일 HH:mm', { locale: ko }); + } catch (formatError) { + // date-fns 포맷 실패 시 수동 포맷 사용 + return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; + } } catch (error) { - console.error('날짜 포맷 변환 오류:', error, isoDateStr); + console.error('[날짜 표시] 날짜 포맷 변환 오류:', error, isoDateStr); // 오류 발생 시 기본값 반환 return '날짜 오류'; } diff --git a/src/utils/sync/transaction/downloadTransaction.ts b/src/utils/sync/transaction/downloadTransaction.ts index 88f7a1e..86777a8 100644 --- a/src/utils/sync/transaction/downloadTransaction.ts +++ b/src/utils/sync/transaction/downloadTransaction.ts @@ -13,30 +13,62 @@ export const downloadTransactions = async (userId: string): Promise => { if (!isSyncEnabled()) return; try { - console.log('서버에서 트랜잭션 데이터 다운로드 시작'); - const { data, error } = await supabase - .from('transactions') - .select('*') - .eq('user_id', userId); + console.log('[동기화] 서버에서 트랜잭션 데이터 다운로드 시작'); + + // 대용량 데이터 처리를 위한 페이지네이션 설정 + const pageSize = 500; // 한 번에 가져올 최대 레코드 수 + let lastId = null; + let allServerData = []; + let hasMore = true; + + // 페이지네이션을 사용하여 모든 데이터 가져오기 + while (hasMore) { + let query = supabase + .from('transactions') + .select('*') + .eq('user_id', userId) + .order('id', { ascending: true }) + .limit(pageSize); + + // 마지막 ID 이후의 데이터만 가져오기 + if (lastId) { + query = query.gt('id', lastId); + } - if (error) { - console.error('트랜잭션 다운로드 실패:', error); - throw error; + const { data, error } = await query; + + if (error) { + console.error('[동기화] 트랜잭션 다운로드 실패:', error); + console.error('[동기화] 오류 상세:', JSON.stringify(error, null, 2)); + throw error; + } + + if (!data || data.length === 0) { + hasMore = false; + } else { + allServerData = [...allServerData, ...data]; + lastId = data[data.length - 1].id; + + // 마지막 페이지인지 확인 + if (data.length < pageSize) { + hasMore = false; + } + } } - if (!data || data.length === 0) { - console.log('서버에 저장된 트랜잭션 없음'); + if (allServerData.length === 0) { + console.log('[동기화] 서버에 저장된 트랜잭션 없음'); return; // 서버에 데이터가 없으면 로컬 데이터 유지 } - console.log(`서버에서 ${data.length}개의 트랜잭션 다운로드`); + console.log(`[동기화] 서버에서 ${allServerData.length}개의 트랜잭션 다운로드`); // 삭제된 트랜잭션 ID 목록 가져오기 const deletedIds = getDeletedTransactions(); console.log(`[동기화] 삭제된 트랜잭션 ${deletedIds.length}개 필터링 적용`); // 서버 데이터를 로컬 형식으로 변환 (삭제된 항목 제외) - const serverTransactions = data + const serverTransactions = allServerData .filter(t => { const transactionId = t.transaction_id || t.id; const isDeleted = deletedIds.includes(transactionId); @@ -46,37 +78,51 @@ export const downloadTransactions = async (userId: string): Promise => { return !isDeleted; // 삭제된 항목 제외 }) .map(t => { - // 날짜 형식 변환 시 오류 방지 처리 - let formattedDate = '날짜 없음'; try { - if (t.date) { - // ISO 형식이 아닌 경우 기본 변환 수행 - if (!t.date.match(/^\d{4}-\d{2}-\d{2}T/)) { - console.log(`비표준 날짜 형식 감지: ${t.date}, ID: ${t.transaction_id || t.id}`); - // 유효한 Date 객체로 변환 가능한지 확인 - const testDate = new Date(t.date); - if (isNaN(testDate.getTime())) { - console.warn(`잘못된 날짜 형식 감지, 현재 날짜 사용: ${t.date}`); - t.date = new Date().toISOString(); // 잘못된 날짜는 현재 날짜로 대체 + // 날짜 형식 변환 시 오류 방지 처리 + let formattedDate = '날짜 없음'; + try { + if (t.date) { + // ISO 형식이 아닌 경우 기본 변환 수행 + if (!t.date.match(/^\d{4}-\d{2}-\d{2}T/)) { + console.log(`[동기화] 비표준 날짜 형식 감지: ${t.date}, ID: ${t.transaction_id || t.id}`); + // 유효한 Date 객체로 변환 가능한지 확인 + const testDate = new Date(t.date); + if (isNaN(testDate.getTime())) { + console.warn(`[동기화] 잘못된 날짜 형식 감지, 현재 날짜 사용: ${t.date}`); + t.date = new Date().toISOString(); // 잘못된 날짜는 현재 날짜로 대체 + } } + formattedDate = formatDateForDisplay(t.date); } - formattedDate = formatDateForDisplay(t.date); + } catch (err) { + console.error(`[동기화] 날짜 변환 오류 (ID: ${t.transaction_id || t.id}):`, err); + // 오류 발생 시 기본값 사용 + formattedDate = new Date().toLocaleString('ko-KR'); } - } catch (err) { - console.error(`날짜 변환 오류 (ID: ${t.transaction_id || t.id}):`, err); - // 오류 발생 시 기본값 사용 - formattedDate = new Date().toLocaleString('ko-KR'); + + return { + id: t.transaction_id || t.id, + title: t.title || '무제', + amount: t.amount || 0, + date: formattedDate, + category: t.category || '기타', + type: t.type || 'expense', + notes: t.notes || '' + }; + } catch (itemError) { + console.error(`[동기화] 트랜잭션 변환 오류 (ID: ${t.transaction_id || t.id}):`, itemError); + // 오류 발생 시 기본 객체 반환 + return { + id: t.transaction_id || t.id, + title: '데이터 오류', + amount: 0, + date: new Date().toLocaleString('ko-KR'), + category: '기타', + type: 'expense', + notes: '데이터 변환 중 오류 발생' + }; } - - return { - id: t.transaction_id || t.id, - title: t.title, - amount: t.amount, - date: formattedDate, - category: t.category, - type: t.type, - notes: t.notes - }; }); // 기존 로컬 데이터 불러오기 @@ -88,12 +134,16 @@ export const downloadTransactions = async (userId: string): Promise => { // 로컬 데이터를 맵에 추가 localTransactions.forEach((tx: Transaction) => { - transactionMap.set(tx.id, tx); + if (tx && tx.id) { // 유효성 검사 추가 + transactionMap.set(tx.id, tx); + } }); // 서버 데이터로 맵 업데이트 (서버 데이터 우선) serverTransactions.forEach(tx => { - transactionMap.set(tx.id, tx); + if (tx && tx.id) { // 유효성 검사 추가 + transactionMap.set(tx.id, tx); + } }); // 최종 병합된 데이터 생성 @@ -101,12 +151,13 @@ export const downloadTransactions = async (userId: string): Promise => { // 로컬 스토리지에 저장 localStorage.setItem('transactions', JSON.stringify(mergedTransactions)); - console.log(`총 ${mergedTransactions.length}개의 트랜잭션 병합 완료`); + console.log(`[동기화] 총 ${mergedTransactions.length}개의 트랜잭션 병합 완료`); // 이벤트 발생시켜 UI 업데이트 window.dispatchEvent(new Event('transactionUpdated')); } catch (error) { - console.error('트랜잭션 다운로드 중 오류:', error); + console.error('[동기화] 트랜잭션 다운로드 중 오류:', error); + console.error('[동기화] 오류 상세:', JSON.stringify(error, null, 2)); throw error; } }; diff --git a/src/utils/sync/transaction/uploadTransaction.ts b/src/utils/sync/transaction/uploadTransaction.ts index a312dbe..f87d204 100644 --- a/src/utils/sync/transaction/uploadTransaction.ts +++ b/src/utils/sync/transaction/uploadTransaction.ts @@ -27,7 +27,8 @@ export const uploadTransactions = async (userId: string): Promise => { .eq('user_id', userId); if (fetchError) { - console.error('기존 트랜잭션 조회 실패:', fetchError); + console.error('[동기화] 기존 트랜잭션 조회 실패:', fetchError); + console.error('[동기화] 오류 상세:', JSON.stringify(fetchError, null, 2)); throw fetchError; } @@ -40,62 +41,92 @@ export const uploadTransactions = async (userId: string): Promise => { const updateTransactions = []; for (const t of transactions) { - // 날짜 형식 정규화 - const normalizedDate = normalizeDate(t.date); - - const transactionData = { - user_id: userId, - title: t.title, - amount: t.amount, - date: normalizedDate, // 정규화된 날짜 사용 - category: t.category, - type: t.type, - transaction_id: t.id, - notes: t.notes || null - }; - - if (existingIds.has(t.id)) { - updateTransactions.push(transactionData); - } else { - newTransactions.push(transactionData); + try { + // 날짜 형식 정규화 + const normalizedDate = normalizeDate(t.date); + + const transactionData = { + user_id: userId, + title: t.title || '무제', + amount: t.amount || 0, + date: normalizedDate, // 정규화된 날짜 사용 + category: t.category || '기타', + type: t.type || 'expense', + transaction_id: t.id, + notes: t.notes || null + }; + + if (existingIds.has(t.id)) { + updateTransactions.push(transactionData); + } else { + newTransactions.push(transactionData); + } + } catch (err) { + console.error(`[동기화] 트랜잭션 처리 중 오류 (ID: ${t.id}):`, err); + // 개별 트랜잭션 오류는 기록하고 계속 진행 } } - // 새 트랜잭션 삽입 (있는 경우) + // 새 트랜잭션 삽입 (있는 경우) - 배치 처리 if (newTransactions.length > 0) { console.log(`${newTransactions.length}개의 새 트랜잭션 업로드`); - const { error: insertError } = await supabase - .from('transactions') - .insert(newTransactions); - - if (insertError) { - console.error('새 트랜잭션 업로드 실패:', insertError); - throw insertError; - } - } - - // 기존 트랜잭션 업데이트 (있는 경우) - if (updateTransactions.length > 0) { - console.log(`${updateTransactions.length}개의 기존 트랜잭션 업데이트`); - // 각 트랜잭션을 개별적으로 업데이트 - for (const transaction of updateTransactions) { - const { error: updateError } = await supabase + // 대용량 데이터 처리를 위해 배치 처리 (최대 100개씩) + const batchSize = 100; + for (let i = 0; i < newTransactions.length; i += batchSize) { + const batch = newTransactions.slice(i, i + batchSize); + const { error: insertError } = await supabase .from('transactions') - .update(transaction) - .eq('transaction_id', transaction.transaction_id) - .eq('user_id', userId); + .insert(batch); - if (updateError) { - console.error('트랜잭션 업데이트 실패:', updateError, transaction); - // 실패해도 계속 진행 + if (insertError) { + console.error(`[동기화] 새 트랜잭션 배치 업로드 실패 (${i}~${i + batch.length}):`, insertError); + console.error('[동기화] 오류 상세:', JSON.stringify(insertError, null, 2)); + // 배치 실패해도 다음 배치 계속 시도 } } } - console.log('트랜잭션 업로드 완료'); + // 기존 트랜잭션 업데이트 (있는 경우) - 배치 처리 + if (updateTransactions.length > 0) { + console.log(`${updateTransactions.length}개의 기존 트랜잭션 업데이트`); + + // 대용량 데이터 처리를 위해 배치 처리 (최대 50개씩) + // 업데이트는 개별 쿼리보다 효율적이지만 삽입보다는 복잡하므로 더 작은 배치 크기 사용 + const batchSize = 50; + for (let i = 0; i < updateTransactions.length; i += batchSize) { + const batch = updateTransactions.slice(i, i + batchSize); + + // 배치 내 트랜잭션을 병렬로 업데이트 (Promise.all 사용) + const updatePromises = batch.map(transaction => + supabase + .from('transactions') + .update(transaction) + .eq('transaction_id', transaction.transaction_id) + .eq('user_id', userId) + ); + + try { + const results = await Promise.all(updatePromises); + // 오류 확인 + const errors = results.filter(result => result.error); + if (errors.length > 0) { + console.error(`[동기화] ${errors.length}개의 트랜잭션 업데이트 실패`); + errors.forEach(err => { + console.error('[동기화] 업데이트 오류:', err.error); + }); + } + } catch (batchError) { + console.error(`[동기화] 트랜잭션 배치 업데이트 실패 (${i}~${i + batch.length}):`, batchError); + // 배치 실패해도 다음 배치 계속 시도 + } + } + } + + console.log('[동기화] 트랜잭션 업로드 완료'); } catch (error) { - console.error('트랜잭션 업로드 실패:', error); + console.error('[동기화] 트랜잭션 업로드 실패:', error); + console.error('[동기화] 오류 상세:', JSON.stringify(error, null, 2)); throw error; } }; diff --git a/src/utils/syncUtils.ts b/src/utils/syncUtils.ts index afbe881..e39ccc1 100644 --- a/src/utils/syncUtils.ts +++ b/src/utils/syncUtils.ts @@ -1,60 +1,47 @@ -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 { downloadTransactions, uploadTransactions } from './sync/transaction'; +import { downloadBudgets, uploadBudgets } from './sync/budget'; +import { isSyncEnabled, getLastSyncTime, setLastSyncTime } from './sync/syncSettings'; +import { RetryOptions } from './network/types'; +import { withRetry } from './network/retry'; import { - checkNetworkStatus, getNetworkStatus, setNetworkStatus, startNetworkMonitoring, stopNetworkMonitoring, - withRetry, - addToSyncQueue, - processPendingSyncQueue, onNetworkStatusChange, NetworkStatus, - RetryOptions + addToSyncQueue, + processPendingSyncQueue } from './networkUtils'; // Export all utility functions to maintain the same public API export { isSyncEnabled, - setSyncEnabled, - uploadTransactions, - downloadTransactions, - deleteTransactionFromServer, - uploadBudgets, - downloadBudgets, - getLastSyncTime, - setLastSyncTime, - initSyncSettings, - clearCloudData, - // 네트워크 관련 함수 추가 내보내기 - checkNetworkStatus, getNetworkStatus, + setNetworkStatus, startNetworkMonitoring, stopNetworkMonitoring, - onNetworkStatusChange + onNetworkStatusChange, + addToSyncQueue, + processPendingSyncQueue, + getLastSyncTime, + setLastSyncTime }; /** * 동기화 상태 인터페이스 */ export interface SyncState { - isEnabled: boolean; - lastSyncTime: string | null; - networkStatus: NetworkStatus; - isSyncing: boolean; - error: string | null; + status: 'idle' | 'syncing' | 'success' | 'error' | 'partial'; + message?: string; + lastSyncTime?: string; } // 현재 동기화 상태 let syncState: SyncState = { - isEnabled: false, - lastSyncTime: null, - networkStatus: 'online', - isSyncing: false, - error: null + status: 'idle', + message: undefined, + lastSyncTime: getLastSyncTime() }; /** @@ -62,11 +49,9 @@ let syncState: SyncState = { */ export const initSyncState = async (): Promise => { syncState = { - isEnabled: isSyncEnabled(), - lastSyncTime: getLastSyncTime(), - networkStatus: getNetworkStatus(), - isSyncing: false, - error: null + status: 'idle', + message: undefined, + lastSyncTime: getLastSyncTime() }; // 네트워크 모니터링 시작 @@ -74,7 +59,7 @@ export const initSyncState = async (): Promise => { // 네트워크 상태 변경 리스너 등록 onNetworkStatusChange((status) => { - syncState.networkStatus = status; + syncState.status = status === 'online' ? 'idle' : 'error'; // 상태 변경 이벤트 발생 window.dispatchEvent(new CustomEvent('syncStateChange', { detail: { ...syncState } })); @@ -124,188 +109,155 @@ export const onSyncStateChange = (callback: (state: SyncState) => void): () => v }; /** - * Synchronize all data with Supabase + * 데이터 동기화를 위한 재시도 로직이 포함된 유틸리티 함수 + * @param fn 실행할 함수 + * @param options 재시도 옵션 */ -export const syncAllData = async (userId: string): Promise => { - if (!userId || !isSyncEnabled()) return; +// export const withRetry = async ( +// fn: () => Promise, +// options: RetryOptions +// ): Promise => { +// const { entityType, maxRetries = 3, retryDelay = 1500 } = options; +// let lastError: Error | unknown = null; - // 네트워크 상태 확인 - const isOnline = await checkNetworkStatus(); - if (!isOnline) { - const error = new Error('오프라인 상태에서 동기화할 수 없습니다.'); - console.error('[동기화] 오류:', error); - updateSyncState({ error: error.message }); - throw error; - } +// for (let attempt = 1; attempt <= maxRetries; attempt++) { +// try { +// if (attempt > 1) { +// console.log(`[동기화] ${entityType} 재시도 중... (${attempt}/${maxRetries})`); +// } + +// const result = await fn(); +// if (attempt > 1) { +// console.log(`[동기화] ${entityType} 재시도 성공! (${attempt}/${maxRetries})`); +// } +// return result; +// } catch (error) { +// lastError = error; +// console.error(`[동기화] ${entityType} 실패 (시도 ${attempt}/${maxRetries}):`, error); + +// // 자세한 오류 정보 로깅 +// try { +// console.error(`[동기화] 오류 상세 정보:`, JSON.stringify(error, null, 2)); +// } catch (jsonError) { +// console.error(`[동기화] 오류 객체를 JSON으로 변환할 수 없음:`, error); +// } + +// if (attempt < maxRetries) { +// console.log(`[동기화] ${retryDelay}ms 후 재시도...`); +// await new Promise(resolve => setTimeout(resolve, retryDelay)); +// } +// } +// } - // 동기화 상태 업데이트 - updateSyncState({ isSyncing: true, error: null }); - - try { - console.log('[동기화] 데이터 동기화 시작...'); - - // 기존 동기화 순서: 서버에서 먼저 다운로드 후, 로컬 데이터 업로드 - // 이 순서를 유지하여 서버에 저장된 데이터를 먼저 가져온 후, 로컬 변경사항을 반영 - - // 1. 서버에서 데이터 다운로드 (기존 데이터 불러오기) - await withRetry( - () => downloadTransactions(userId), - { entityType: '트랜잭션 다운로드' } - ); - - await withRetry( - () => downloadBudgets(userId), - { entityType: '예산 다운로드' } - ); - - // 약간의 딜레이를 추가하여 다운로드된 데이터가 처리될 시간을 줌 - await new Promise(resolve => setTimeout(resolve, 500)); - - // 2. 로컬 데이터를 서버에 업로드 (변경사항 반영) - await withRetry( - () => uploadTransactions(userId), - { entityType: '트랜잭션 업로드' } - ); - - await withRetry( - () => uploadBudgets(userId), - { entityType: '예산 업로드' } - ); - - // 동기화 시간 업데이트 - setLastSyncTime(); - updateSyncState({ - lastSyncTime: getLastSyncTime(), - isSyncing: false - }); - - console.log('[동기화] 데이터 동기화 완료!'); - } catch (error) { - console.error('[동기화] 오류 발생:', error); - updateSyncState({ - isSyncing: false, - error: error instanceof Error ? error.message : '알 수 없는 오류' - }); - throw error; // 오류를 상위 호출자에게 전달하여 적절히 처리하도록 함 - } -}; +// console.error(`[동기화] ${entityType} 최대 재시도 횟수(${maxRetries}) 초과, 실패`); +// throw lastError; +// }; -// 동기화 결과를 위한 인터페이스 정의 -export interface SyncResult { - success: boolean; - partial?: boolean; - downloadSuccess?: boolean; - uploadSuccess?: boolean; - 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 - }; +/** + * 모든 데이터를 동기화하는 함수 + * 다운로드 및 업로드 작업을 순차적으로 수행 + */ +export const trySyncAllData = async ( + userId: string, + setSyncState: (state: SyncState) => void +): Promise => { + if (!userId) { + console.error('[동기화] 사용자 ID가 없어 동기화를 진행할 수 없습니다.'); + return; } - - // 동기화 상태 업데이트 - updateSyncState({ isSyncing: true, error: null }); - - let downloadSuccess = false; - let uploadSuccess = false; - let error = null; - + try { - console.log('[동기화] 안전한 데이터 동기화 시도...'); + setSyncState({ status: 'syncing', message: '동기화 중...' }); + // 네트워크 연결 확인 + if (!navigator.onLine) { + setSyncState({ status: 'error', message: '네트워크 연결이 없습니다.' }); + return; + } + + console.log('[동기화] 데이터 동기화 시작'); + const startTime = Date.now(); + + // 1. 다운로드 작업 (서버 → 로컬) try { - // 1단계: 서버에서 데이터 다운로드 + console.log('[동기화] 다운로드 작업 시작'); + + // 트랜잭션 다운로드 await withRetry( () => downloadTransactions(userId), - { entityType: '트랜잭션 다운로드', maxRetries: 2 } + { entityType: '트랜잭션 다운로드', maxRetries: 3, retryDelay: 1500 } ); + // 예산 다운로드 await withRetry( () => downloadBudgets(userId), - { entityType: '예산 다운로드', maxRetries: 2 } + { entityType: '예산 다운로드', maxRetries: 3, retryDelay: 1500 } ); - console.log('[동기화] 서버 데이터 다운로드 성공'); - downloadSuccess = true; - - // 다운로드 단계가 성공적으로 완료되면 부분 동기화 마킹 - setLastSyncTime('부분-다운로드'); - updateSyncState({ lastSyncTime: getLastSyncTime() }); + console.log('[동기화] 다운로드 작업 완료'); } catch (downloadError) { - console.error('[동기화] 다운로드 오류:', downloadError); - error = downloadError; - // 다운로드 실패해도 업로드는 시도 - 부분 동기화 + console.error('[동기화] 다운로드 작업 실패:', downloadError); + setSyncState({ + status: 'error', + message: `다운로드 중 오류가 발생했습니다: ${downloadError instanceof Error ? downloadError.message : '알 수 없는 오류'}` + }); + return; // 다운로드 실패 시 업로드 작업 진행하지 않음 } - // 다운로드 후 약간의 지연을 추가하여 로컬 상태가 업데이트될 시간을 줌 - await new Promise(resolve => setTimeout(resolve, 500)); - + // 2. 업로드 작업 (로컬 → 서버) try { - // 2단계: 로컬 데이터를 서버에 업로드 + console.log('[동기화] 업로드 작업 시작'); + + // 트랜잭션 업로드 await withRetry( () => uploadTransactions(userId), - { entityType: '트랜잭션 업로드', maxRetries: 2 } + { entityType: '트랜잭션 업로드', maxRetries: 3, retryDelay: 1500 } ); + // 예산 업로드 await withRetry( () => uploadBudgets(userId), - { entityType: '예산 업로드', maxRetries: 2 } + { entityType: '예산 업로드', maxRetries: 3, retryDelay: 1500 } ); - console.log('[동기화] 로컬 데이터 업로드 성공'); - uploadSuccess = true; - - // 업로드까지 성공적으로 완료되면 동기화 시간 업데이트 - setLastSyncTime(); - updateSyncState({ lastSyncTime: getLastSyncTime() }); + console.log('[동기화] 업로드 작업 완료'); } catch (uploadError) { - console.error('[동기화] 업로드 오류:', uploadError); - if (!error) error = uploadError; // 다운로드에서 오류가 없었을 경우에만 설정 - } - - // 동기화 상태 업데이트 - updateSyncState({ isSyncing: false }); - - // 결과 반환 - const success = downloadSuccess && uploadSuccess; - const partial = (downloadSuccess || uploadSuccess) && !(downloadSuccess && uploadSuccess); - - if (!success && error) { - updateSyncState({ - error: error instanceof Error ? error.message : '알 수 없는 오류' + console.error('[동기화] 업로드 작업 실패:', uploadError); + // 업로드 실패 시에도 일부 데이터는 동기화되었을 수 있으므로 부분 성공으로 처리 + setSyncState({ + status: 'partial', + message: `일부 데이터 동기화 중 오류가 발생했습니다: ${uploadError instanceof Error ? uploadError.message : '알 수 없는 오류'}` }); + return; } - return { - success, - partial, - downloadSuccess, - uploadSuccess, - error: error ? (error instanceof Error ? error.message : error) : undefined - }; - } catch (generalError) { - console.error('[동기화] 일반 오류:', generalError); - updateSyncState({ - isSyncing: false, - error: generalError instanceof Error ? generalError.message : '알 수 없는 오류' + // 동기화 완료 시간 기록 + const endTime = Date.now(); + const syncDuration = (endTime - startTime) / 1000; // 초 단위 + + console.log(`[동기화] 모든 데이터 동기화 완료 (${syncDuration.toFixed(2)}초 소요)`); + setSyncState({ + status: 'success', + message: `동기화 완료 (${syncDuration.toFixed(1)}초)` }); - return { - success: false, - error: generalError - }; + // 동기화 완료 시간 저장 + setLastSyncTime(new Date().toISOString()); + + } catch (error) { + console.error('[동기화] 동기화 중 예상치 못한 오류:', error); + + // 자세한 오류 정보 로깅 + try { + console.error('[동기화] 오류 상세 정보:', JSON.stringify(error, null, 2)); + } catch (jsonError) { + console.error('[동기화] 오류 객체를 JSON으로 변환할 수 없음:', error); + } + + setSyncState({ + status: 'error', + message: `동기화 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}` + }); } };