From 335848cca89033ab16d768b9760912d2943d5983 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:42:18 +0000 Subject: [PATCH] Refactor sync data module Splits the large `src/utils/sync/data.ts` file into smaller, more manageable modules to improve code organization, readability, and maintainability. --- src/utils/sync/core/backup.ts | 38 +++++ src/utils/sync/core/index.ts | 17 +++ src/utils/sync/core/recovery.ts | 18 +++ src/utils/sync/core/status.ts | 91 +++++++++++ src/utils/sync/core/syncOperations.ts | 106 +++++++++++++ src/utils/sync/core/types.ts | 45 ++++++ src/utils/sync/data.ts | 211 +++++++------------------- 7 files changed, 374 insertions(+), 152 deletions(-) create mode 100644 src/utils/sync/core/backup.ts create mode 100644 src/utils/sync/core/index.ts create mode 100644 src/utils/sync/core/recovery.ts create mode 100644 src/utils/sync/core/status.ts create mode 100644 src/utils/sync/core/syncOperations.ts create mode 100644 src/utils/sync/core/types.ts diff --git a/src/utils/sync/core/backup.ts b/src/utils/sync/core/backup.ts new file mode 100644 index 0000000..5505054 --- /dev/null +++ b/src/utils/sync/core/backup.ts @@ -0,0 +1,38 @@ + +/** + * 데이터 백업 및 복원 관련 기능 + */ +import { BackupData } from './types'; + +/** + * 현재 로컬 데이터를 백업 + */ +export const backupLocalData = (): BackupData => { + return { + budgetData: localStorage.getItem('budgetData'), + categoryBudgets: localStorage.getItem('categoryBudgets'), + transactions: localStorage.getItem('transactions') + }; +}; + +/** + * 백업 데이터를 로컬 스토리지에 복원 + */ +export const restoreLocalData = (backup: BackupData): void => { + if (backup.budgetData) { + localStorage.setItem('budgetData', backup.budgetData); + } + + if (backup.categoryBudgets) { + localStorage.setItem('categoryBudgets', backup.categoryBudgets); + } + + if (backup.transactions) { + localStorage.setItem('transactions', backup.transactions); + } + + // UI 업데이트 이벤트 발생 + window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event('categoryBudgetsUpdated')); + window.dispatchEvent(new Event('transactionUpdated')); +}; diff --git a/src/utils/sync/core/index.ts b/src/utils/sync/core/index.ts new file mode 100644 index 0000000..ca97e18 --- /dev/null +++ b/src/utils/sync/core/index.ts @@ -0,0 +1,17 @@ + +/** + * 동기화 코어 모듈 + */ + +// 타입 정의 내보내기 +export * from './types'; + +// 백업 및 복구 기능 내보내기 +export * from './backup'; +export * from './recovery'; + +// 상태 확인 기능 내보내기 +export * from './status'; + +// 동기화 작업 내보내기 +export * from './syncOperations'; diff --git a/src/utils/sync/core/recovery.ts b/src/utils/sync/core/recovery.ts new file mode 100644 index 0000000..e31ac81 --- /dev/null +++ b/src/utils/sync/core/recovery.ts @@ -0,0 +1,18 @@ + +/** + * 동기화 과정에서 오류 발생 시 복구 기능 + */ +import { BackupData } from './types'; +import { restoreLocalData } from './backup'; + +/** + * 동기화 오류 시 데이터 복구 + */ +export const recoverFromSyncError = (backup: BackupData, error: unknown): void => { + console.error('데이터 동기화 중 치명적 오류:', error); + + // 백업 데이터 복원 + restoreLocalData(backup); + + console.log('백업 데이터 복원 완료'); +}; diff --git a/src/utils/sync/core/status.ts b/src/utils/sync/core/status.ts new file mode 100644 index 0000000..a765d9e --- /dev/null +++ b/src/utils/sync/core/status.ts @@ -0,0 +1,91 @@ + +/** + * 서버 및 로컬 데이터 상태 확인 기능 + */ +import { supabase } from '@/lib/supabase'; +import { DataStatus, ServerDataStatus, LocalDataStatus } from './types'; + +/** + * 서버에 데이터가 있는지 확인 + */ +export const checkServerDataStatus = async (userId: string): Promise => { + try { + // 서버에 예산 데이터 확인 + const { data: budgetData } = await supabase + .from('budgets') + .select('count') + .eq('user_id', userId) + .single(); + + // 서버에 트랜잭션 데이터 확인 + const { data: transactionsData } = await supabase + .from('transactions') + .select('count') + .eq('user_id', userId) + .single(); + + const hasBudgetData = (budgetData?.count || 0) > 0; + const hasTransactionData = (transactionsData?.count || 0) > 0; + + return { + hasBudgetData, + hasTransactionData, + hasData: hasBudgetData || hasTransactionData + }; + } catch (error) { + console.error('서버 데이터 상태 확인 오류:', error); + return { + hasBudgetData: false, + hasTransactionData: false, + hasData: false + }; + } +}; + +/** + * 로컬 데이터가 있는지 확인 + */ +export const checkLocalDataStatus = (): LocalDataStatus => { + try { + const budgetDataStr = localStorage.getItem('budgetData'); + const transactionsStr = localStorage.getItem('transactions'); + + let hasBudgetData = false; + if (budgetDataStr) { + const budgetData = JSON.parse(budgetDataStr); + hasBudgetData = budgetData?.monthly?.targetAmount > 0; + } + + let hasTransactionData = false; + if (transactionsStr) { + const transactions = JSON.parse(transactionsStr); + hasTransactionData = Array.isArray(transactions) && transactions.length > 0; + } + + return { + hasBudgetData, + hasTransactionData, + hasData: hasBudgetData || hasTransactionData + }; + } catch (error) { + console.error('로컬 데이터 상태 확인 오류:', error); + return { + hasBudgetData: false, + hasTransactionData: false, + hasData: false + }; + } +}; + +/** + * 서버와 로컬 데이터 상태 모두 확인 + */ +export const checkDataStatus = async (userId: string): Promise => { + const serverStatus = await checkServerDataStatus(userId); + const localStatus = checkLocalDataStatus(); + + return { + server: serverStatus, + local: localStatus + }; +}; diff --git a/src/utils/sync/core/syncOperations.ts b/src/utils/sync/core/syncOperations.ts new file mode 100644 index 0000000..ff8d8d3 --- /dev/null +++ b/src/utils/sync/core/syncOperations.ts @@ -0,0 +1,106 @@ + +/** + * 주요 동기화 작업 처리 모듈 + */ +import { uploadBudgets, downloadBudgets } from '../budget'; +import { uploadTransactions, downloadTransactions } from '../transaction'; +import { setLastSyncTime } from '../time'; +import { SyncResult } from './types'; + +/** + * 데이터 업로드 수행 + */ +export const performUpload = async (userId: string): Promise<{ + budgetUpload: boolean; + transactionUpload: boolean; +}> => { + try { + console.log('데이터 업로드 시작'); + + // 예산 데이터 업로드 + let budgetUpload = false; + try { + await uploadBudgets(userId); + budgetUpload = true; + console.log('예산 업로드 성공'); + } catch (error) { + console.error('예산 업로드 실패:', error); + } + + // 트랜잭션 데이터 업로드 + let transactionUpload = false; + try { + await uploadTransactions(userId); + transactionUpload = true; + console.log('트랜잭션 업로드 성공'); + } catch (error) { + console.error('트랜잭션 업로드 실패:', error); + } + + return { budgetUpload, transactionUpload }; + } catch (error) { + console.error('업로드 작업 실패:', error); + return { budgetUpload: false, transactionUpload: false }; + } +}; + +/** + * 데이터 다운로드 수행 + */ +export const performDownload = async (userId: string): Promise<{ + budgetDownload: boolean; + transactionDownload: boolean; +}> => { + try { + console.log('데이터 다운로드 시작'); + + // 예산 데이터 다운로드 + let budgetDownload = false; + try { + await downloadBudgets(userId); + budgetDownload = true; + console.log('예산 다운로드 성공'); + } catch (error) { + console.error('예산 다운로드 실패:', error); + } + + // 트랜잭션 데이터 다운로드 + let transactionDownload = false; + try { + await downloadTransactions(userId); + transactionDownload = true; + console.log('트랜잭션 다운로드 성공'); + } catch (error) { + console.error('트랜잭션 다운로드 실패:', error); + } + + return { budgetDownload, transactionDownload }; + } catch (error) { + console.error('다운로드 작업 실패:', error); + return { budgetDownload: false, transactionDownload: false }; + } +}; + +/** + * 동기화 결과 객체 생성 + */ +export const createSyncResult = ( + uploadResult: { budgetUpload: boolean; transactionUpload: boolean }, + downloadResult: { budgetDownload: boolean; transactionDownload: boolean } +): SyncResult => { + const uploadSuccess = uploadResult.budgetUpload || uploadResult.transactionUpload; + const downloadSuccess = downloadResult.budgetDownload || downloadResult.transactionDownload; + + return { + success: uploadSuccess || downloadSuccess, + partial: (uploadSuccess || downloadSuccess) && !(uploadSuccess && downloadSuccess), + uploadSuccess, + downloadSuccess, + details: { + budgetUpload: uploadResult.budgetUpload, + budgetDownload: downloadResult.budgetDownload, + transactionUpload: uploadResult.transactionUpload, + transactionDownload: downloadResult.transactionDownload + } + }; +}; diff --git a/src/utils/sync/core/types.ts b/src/utils/sync/core/types.ts new file mode 100644 index 0000000..cd5a9bd --- /dev/null +++ b/src/utils/sync/core/types.ts @@ -0,0 +1,45 @@ + +/** + * 동기화 관련 타입 정의 + */ + +// 동기화 결과 타입 정의 +export interface SyncResult { + success: boolean; + partial: boolean; + uploadSuccess: boolean; + downloadSuccess: boolean; + details?: { + budgetUpload?: boolean; + budgetDownload?: boolean; + transactionUpload?: boolean; + transactionDownload?: boolean; + }; +} + +// 백업 데이터 타입 정의 +export interface BackupData { + budgetData: string | null; + categoryBudgets: string | null; + transactions: string | null; +} + +// 서버 데이터 상태 타입 +export interface ServerDataStatus { + hasBudgetData: boolean; + hasTransactionData: boolean; + hasData: boolean; +} + +// 로컬 데이터 상태 타입 +export interface LocalDataStatus { + hasBudgetData: boolean; + hasTransactionData: boolean; + hasData: boolean; +} + +// 데이터 상태 타입 +export interface DataStatus { + server: ServerDataStatus; + local: LocalDataStatus; +} diff --git a/src/utils/sync/data.ts b/src/utils/sync/data.ts index 7246a9f..6e90eb8 100644 --- a/src/utils/sync/data.ts +++ b/src/utils/sync/data.ts @@ -1,29 +1,28 @@ -import { supabase } from '@/lib/supabase'; -import { uploadBudgets, downloadBudgets } from './budget'; -import { uploadTransactions, downloadTransactions } from './transaction'; +/** + * 데이터 동기화 메인 모듈 + */ +import { isSyncEnabled } from './config'; import { setLastSyncTime } from './time'; +import { + BackupData, + SyncResult, + backupLocalData, + recoverFromSyncError, + checkDataStatus, + performUpload, + performDownload, + createSyncResult +} from './core'; -export interface SyncResult { - success: boolean; - partial: boolean; - uploadSuccess: boolean; - downloadSuccess: boolean; - details?: { - budgetUpload?: boolean; - budgetDownload?: boolean; - transactionUpload?: boolean; - transactionDownload?: boolean; - }; -} - -// 모든 데이터를 동기화합니다 (업로드 우선 수행) +/** + * 모든 데이터를 동기화합니다 (서버/로컬 상태에 따라 업로드/다운로드 순서 결정) + */ export const syncAllData = async (userId: string): Promise => { // 로컬 데이터 백업 - const backupBudgetData = localStorage.getItem('budgetData'); - const backupCategoryBudgets = localStorage.getItem('categoryBudgets'); - const backupTransactions = localStorage.getItem('transactions'); + const backup: BackupData = backupLocalData(); + // 기본 결과 객체 생성 const result: SyncResult = { success: false, partial: false, @@ -40,132 +39,55 @@ export const syncAllData = async (userId: string): Promise => { try { console.log('데이터 동기화 시작 - 사용자 ID:', userId); - // 서버에 데이터가 있는지 먼저 확인 - const { data: budgetData } = await supabase - .from('budgets') - .select('count') - .eq('user_id', userId) - .single(); - - const { data: transactionsData } = await supabase - .from('transactions') - .select('count') - .eq('user_id', userId) - .single(); - - const serverHasBudgetData = (budgetData?.count || 0) > 0; - const serverHasTransactionData = (transactionsData?.count || 0) > 0; - const serverHasData = serverHasBudgetData || serverHasTransactionData; - - // 로컬 데이터 존재 여부 확인 - const localHasBudgetData = backupBudgetData && JSON.parse(backupBudgetData).monthly.targetAmount > 0; - const localHasTransactionData = backupTransactions && JSON.parse(backupTransactions).length > 0; - const localHasData = localHasBudgetData || localHasTransactionData; + // 동기화 기능이 활성화되어 있는지 확인 + if (!isSyncEnabled()) { + console.log('동기화가 비활성화되어 있어 작업을 건너뜁니다.'); + return result; + } + // 서버와 로컬 데이터 상태 확인 + const status = await checkDataStatus(userId); console.log('데이터 존재 여부:', { 서버: { - 예산: serverHasBudgetData, - 거래: serverHasTransactionData + 예산: status.server.hasBudgetData, + 거래: status.server.hasTransactionData }, 로컬: { - 예산: localHasBudgetData, - 거래: localHasTransactionData + 예산: status.local.hasBudgetData, + 거래: status.local.hasTransactionData } }); - // 서버에 데이터가 없고 로컬에 데이터가 있으면 업로드 먼저 - if (!serverHasData && localHasData) { + // 동기화 전략 결정 - 서버에 데이터가 없고 로컬에 데이터가 있으면 업로드 먼저 + if (!status.server.hasData && status.local.hasData) { console.log('서버에 데이터가 없고 로컬에 데이터가 있어 업로드 우선 실행'); - try { - // 예산 데이터 업로드 - if (localHasBudgetData) { - await uploadBudgets(userId); - result.details!.budgetUpload = true; - console.log('예산 업로드 성공'); - } - - // 트랜잭션 데이터 업로드 - if (localHasTransactionData) { - await uploadTransactions(userId); - result.details!.transactionUpload = true; - console.log('트랜잭션 업로드 성공'); - } - - // 업로드 성공 설정 - result.uploadSuccess = true; - } catch (uploadError) { - console.error('데이터 업로드 실패:', uploadError); - result.uploadSuccess = false; - } + // 업로드 수행 + const uploadResult = await performUpload(userId); + result.uploadSuccess = uploadResult.budgetUpload || uploadResult.transactionUpload; + result.details!.budgetUpload = uploadResult.budgetUpload; + result.details!.transactionUpload = uploadResult.transactionUpload; + } - // 서버에 데이터가 있으면 다운로드 먼저 (기존 로직) - else if (serverHasData) { + // 서버에 데이터가 있으면 다운로드 먼저 + else if (status.server.hasData) { console.log('서버에 데이터가 있어 다운로드 우선 실행'); - try { - // 예산 데이터 다운로드 - if (serverHasBudgetData) { - await downloadBudgets(userId); - result.details!.budgetDownload = true; - console.log('예산 다운로드 성공'); - } - - // 트랜잭션 데이터 다운로드 - if (serverHasTransactionData) { - await downloadTransactions(userId); - result.details!.transactionDownload = true; - console.log('트랜잭션 다운로드 성공'); - } - - // 다운로드 성공 설정 - result.downloadSuccess = true; - } catch (downloadError) { - console.error('데이터 다운로드 실패:', downloadError); - result.downloadSuccess = false; - - // 다운로드 실패 시 로컬 데이터 복원 - if (backupBudgetData) { - localStorage.setItem('budgetData', backupBudgetData); - } - if (backupCategoryBudgets) { - localStorage.setItem('categoryBudgets', backupCategoryBudgets); - } - if (backupTransactions) { - localStorage.setItem('transactions', backupTransactions); - } - - // UI 업데이트 - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - window.dispatchEvent(new Event('transactionUpdated')); - } + // 다운로드 수행 + const downloadResult = await performDownload(userId); + result.downloadSuccess = downloadResult.budgetDownload || downloadResult.transactionDownload; + result.details!.budgetDownload = downloadResult.budgetDownload; + result.details!.transactionDownload = downloadResult.transactionDownload; // 다운로드 후 로컬 데이터 있으면 추가 업로드 시도 - if (result.downloadSuccess && localHasData) { + if (result.downloadSuccess && status.local.hasData) { console.log('다운로드 후 로컬 데이터 업로드 시도'); - try { - // 예산 데이터 업로드 - if (localHasBudgetData) { - await uploadBudgets(userId); - result.details!.budgetUpload = true; - console.log('예산 업로드 성공'); - } - - // 트랜잭션 데이터 업로드 - if (localHasTransactionData) { - await uploadTransactions(userId); - result.details!.transactionUpload = true; - console.log('트랜잭션 업로드 성공'); - } - - // 업로드 성공 설정 - result.uploadSuccess = true; - } catch (uploadError) { - console.error('데이터 업로드 실패:', uploadError); - result.uploadSuccess = false; - } + // 업로드 수행 + const uploadResult = await performUpload(userId); + result.uploadSuccess = uploadResult.budgetUpload || uploadResult.transactionUpload; + result.details!.budgetUpload = uploadResult.budgetUpload; + result.details!.transactionUpload = uploadResult.transactionUpload; } } // 양쪽 다 데이터가 없는 경우 @@ -193,34 +115,16 @@ export const syncAllData = async (userId: string): Promise => { console.log('데이터 동기화 결과:', result); return result; } catch (error) { - console.error('데이터 동기화 중 치명적 오류:', error); - - // 백업 데이터 복원 - if (backupBudgetData) { - localStorage.setItem('budgetData', backupBudgetData); - } - if (backupCategoryBudgets) { - localStorage.setItem('categoryBudgets', backupCategoryBudgets); - } - if (backupTransactions) { - localStorage.setItem('transactions', backupTransactions); - } - - // UI 업데이트 - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - window.dispatchEvent(new Event('transactionUpdated')); - - result.success = false; - result.partial = false; - result.uploadSuccess = false; - result.downloadSuccess = false; + // 오류 발생 시 백업 데이터 복원 + recoverFromSyncError(backup, error); return result; } }; -// 서버에 대한 안전한 동기화 래퍼 +/** + * 서버에 대한 안전한 동기화 래퍼 + */ export const trySyncAllData = async (userId: string): Promise => { console.log('안전한 데이터 동기화 시도'); let attempts = 0; @@ -248,3 +152,6 @@ export const trySyncAllData = async (userId: string): Promise => { return trySync(); }; + +// SyncResult 타입 다시 내보내기 (기존 가져오는 코드와의 호환성) +export type { SyncResult } from './core';