From da282cff5a25e41bcccc888b362117530a317d26 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 01:14:32 +0000 Subject: [PATCH] Fix data loss on sync enable Addresses an issue where budget data was lost when enabling sync after entering budget information. --- src/hooks/useSyncSettings.ts | 41 +++- src/utils/sync/budget/downloadBudget.ts | 53 +++++- src/utils/sync/data.ts | 242 ++++++++++++++++++------ 3 files changed, 268 insertions(+), 68 deletions(-) diff --git a/src/hooks/useSyncSettings.ts b/src/hooks/useSyncSettings.ts index de2147a..2554f09 100644 --- a/src/hooks/useSyncSettings.ts +++ b/src/hooks/useSyncSettings.ts @@ -67,12 +67,49 @@ export const useSyncSettings = () => { return; } + // 현재 로컬 데이터 백업 + const budgetDataBackup = localStorage.getItem('budgetData'); + const categoryBudgetsBackup = localStorage.getItem('categoryBudgets'); + const transactionsBackup = localStorage.getItem('transactions'); + + console.log('동기화 설정 변경 전 로컬 데이터 백업:', { + budgetData: budgetDataBackup ? '있음' : '없음', + categoryBudgets: categoryBudgetsBackup ? '있음' : '없음', + transactions: transactionsBackup ? '있음' : '없음' + }); + setEnabled(checked); setSyncEnabled(checked); if (checked && user) { - // 동기화 활성화 시 즉시 동기화 실행 - await performSync(); + try { + // 동기화 활성화 시 즉시 동기화 실행 + await performSync(); + } catch (error) { + console.error('동기화 중 오류, 로컬 데이터 복원 시도:', error); + + // 오류 발생 시 백업 데이터 복원 + if (budgetDataBackup) { + localStorage.setItem('budgetData', budgetDataBackup); + } + if (categoryBudgetsBackup) { + localStorage.setItem('categoryBudgets', categoryBudgetsBackup); + } + if (transactionsBackup) { + localStorage.setItem('transactions', transactionsBackup); + } + + // 이벤트 발생시켜 UI 업데이트 + window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event('categoryBudgetsUpdated')); + window.dispatchEvent(new Event('transactionUpdated')); + + toast({ + title: "동기화 오류", + description: "동기화 중 문제가 발생하여 로컬 데이터가 복원되었습니다.", + variant: "destructive" + }); + } } }; diff --git a/src/utils/sync/budget/downloadBudget.ts b/src/utils/sync/budget/downloadBudget.ts index 3c204e2..ca5e19e 100644 --- a/src/utils/sync/budget/downloadBudget.ts +++ b/src/utils/sync/budget/downloadBudget.ts @@ -11,6 +11,23 @@ export const downloadBudgets = async (userId: string): Promise => { try { console.log('서버에서 예산 데이터 다운로드 시작'); + // 현재 로컬 예산 데이터 백업 + const localBudgetData = localStorage.getItem('budgetData'); + const localCategoryBudgets = localStorage.getItem('categoryBudgets'); + + // 서버에 데이터가 없는지 확인 + const { data: budgetExists, error: checkError } = await supabase + .from('budgets') + .select('count') + .eq('user_id', userId) + .single(); + + // 서버에 데이터가 없고 로컬에 데이터가 있으면 다운로드 건너뜀 + if ((budgetExists?.count === 0 || !budgetExists) && localBudgetData) { + console.log('서버에 예산 데이터가 없고 로컬 데이터가 있어 다운로드 건너뜀'); + return; + } + // 예산 데이터 및 카테고리 예산 데이터 가져오기 const [budgetData, categoryData] = await Promise.all([ fetchBudgetData(userId), @@ -19,16 +36,24 @@ export const downloadBudgets = async (userId: string): Promise => { // 예산 데이터 처리 if (budgetData) { - await processBudgetData(budgetData); + await processBudgetData(budgetData, localBudgetData); } else { console.log('서버에서 예산 데이터를 찾을 수 없음'); + // 로컬 데이터가 있으면 유지 + if (localBudgetData) { + console.log('로컬 예산 데이터 유지'); + } } // 카테고리 예산 데이터 처리 if (categoryData && categoryData.length > 0) { - await processCategoryBudgetData(categoryData); + await processCategoryBudgetData(categoryData, localCategoryBudgets); } else { console.log('서버에서 카테고리 예산 데이터를 찾을 수 없음'); + // 로컬 데이터가 있으면 유지 + if (localCategoryBudgets) { + console.log('로컬 카테고리 예산 데이터 유지'); + } } console.log('예산 데이터 다운로드 완료'); @@ -82,11 +107,16 @@ async function fetchCategoryBudgetData(userId: string) { /** * 예산 데이터 처리 및 로컬 저장 */ -async function processBudgetData(budgetData: any) { +async function processBudgetData(budgetData: any, localBudgetDataStr: string | null) { console.log('서버에서 예산 데이터 수신:', budgetData); - // 기존 로컬 데이터 가져오기 - const localBudgetDataStr = localStorage.getItem('budgetData'); + // 서버 예산이 0이고 로컬 예산이 있으면 로컬 데이터 유지 + if (budgetData.total_budget === 0 && localBudgetDataStr) { + console.log('서버 예산이 0이고 로컬 예산이 있어 로컬 데이터 유지'); + return; + } + + // 기존 로컬 데이터 가져오기 let localBudgetData = localBudgetDataStr ? JSON.parse(localBudgetDataStr) : { daily: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, weekly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, @@ -114,6 +144,7 @@ async function processBudgetData(budgetData: any) { // 로컬 스토리지에 저장 localStorage.setItem('budgetData', JSON.stringify(updatedBudgetData)); + localStorage.setItem('budgetData_backup', JSON.stringify(updatedBudgetData)); console.log('예산 데이터 로컬 저장 완료', updatedBudgetData); // 이벤트 발생시켜 UI 업데이트 @@ -123,9 +154,18 @@ async function processBudgetData(budgetData: any) { /** * 카테고리 예산 데이터 처리 및 로컬 저장 */ -async function processCategoryBudgetData(categoryData: any[]) { +async function processCategoryBudgetData(categoryData: any[], localCategoryBudgetsStr: string | null) { console.log(`${categoryData.length}개의 카테고리 예산 수신`); + // 서버 카테고리 예산 합계 계산 + const serverTotal = categoryData.reduce((sum, item) => sum + item.amount, 0); + + // 로컬 카테고리 예산이 있고 서버 데이터가 비어있거나 합계가 0이면 로컬 데이터 유지 + if (localCategoryBudgetsStr && (categoryData.length === 0 || serverTotal === 0)) { + console.log('서버 카테고리 예산이 없거나 0이고 로컬 데이터가 있어 로컬 데이터 유지'); + return; + } + // 카테고리 예산 로컬 형식으로 변환 const localCategoryBudgets = categoryData.reduce((acc, curr) => { acc[curr.category] = curr.amount; @@ -134,6 +174,7 @@ async function processCategoryBudgetData(categoryData: any[]) { // 로컬 스토리지에 저장 localStorage.setItem('categoryBudgets', JSON.stringify(localCategoryBudgets)); + localStorage.setItem('categoryBudgets_backup', JSON.stringify(localCategoryBudgets)); console.log('카테고리 예산 로컬 저장 완료', localCategoryBudgets); // 이벤트 발생시켜 UI 업데이트 diff --git a/src/utils/sync/data.ts b/src/utils/sync/data.ts index b574b63..d3915a9 100644 --- a/src/utils/sync/data.ts +++ b/src/utils/sync/data.ts @@ -1,71 +1,193 @@ import { supabase } from '@/lib/supabase'; +import { uploadBudgets, downloadBudgets } from './budget'; +import { uploadTransactions, downloadTransactions } from './transaction'; import { setLastSyncTime } from './time'; +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 => { - if (!userId) { - throw new Error('사용자 ID가 필요합니다'); - } - - try { - // 로컬 트랜잭션 데이터 가져오기 - const transactionsJSON = localStorage.getItem('transactions'); - const transactions = transactionsJSON ? JSON.parse(transactionsJSON) : []; - - // 예산 데이터 가져오기 - const budgetDataJSON = localStorage.getItem('budgetData'); - const budgetData = budgetDataJSON ? JSON.parse(budgetDataJSON) : {}; - - // 카테고리 예산 가져오기 - const categoryBudgetsJSON = localStorage.getItem('categoryBudgets'); - const categoryBudgets = categoryBudgetsJSON ? JSON.parse(categoryBudgetsJSON) : {}; - - // 트랜잭션 데이터 동기화 - for (const transaction of transactions) { - // 이미 동기화된 데이터인지 확인 (transaction_id로 확인) - const { data: existingData } = await supabase - .from('transactions') - .select('*') - .eq('transaction_id', transaction.id) - .eq('user_id', userId); - - // 존재하지 않는 경우에만 삽입 - if (!existingData || existingData.length === 0) { - await supabase.from('transactions').insert({ - user_id: userId, - title: transaction.title, - amount: transaction.amount, - date: transaction.date, - category: transaction.category, - type: transaction.type, - transaction_id: transaction.id - }); - } +export const syncAllData = async (userId: string): Promise => { + // 로컬 데이터 백업 + const backupBudgetData = localStorage.getItem('budgetData'); + const backupCategoryBudgets = localStorage.getItem('categoryBudgets'); + const backupTransactions = localStorage.getItem('transactions'); + + const result: SyncResult = { + success: false, + partial: false, + uploadSuccess: false, + downloadSuccess: false, + details: { + budgetUpload: false, + budgetDownload: false, + transactionUpload: false, + transactionDownload: false } - - // 예산 데이터 동기화 - await supabase.from('budget_data').upsert({ - user_id: userId, - data: budgetData, - updated_at: new Date().toISOString() - }); - - // 카테고리 예산 동기화 - await supabase.from('category_budgets').upsert({ - user_id: userId, - data: categoryBudgets, - updated_at: new Date().toISOString() - }); - - // 마지막 동기화 시간 업데이트 - setLastSyncTime(); + }; + + try { + console.log('데이터 동기화 시작 - 사용자 ID:', userId); - console.log('모든 데이터가 성공적으로 동기화되었습니다'); + // 여기서는 업로드를 먼저 시도합니다 (로컬 데이터 보존을 위해) + try { + // 예산 데이터 업로드 + await uploadBudgets(userId); + result.details!.budgetUpload = true; + console.log('예산 업로드 성공'); + + // 트랜잭션 데이터 업로드 + await uploadTransactions(userId); + result.details!.transactionUpload = true; + console.log('트랜잭션 업로드 성공'); + + // 업로드 성공 설정 + result.uploadSuccess = true; + } catch (uploadError) { + console.error('데이터 업로드 실패:', uploadError); + result.uploadSuccess = false; + } + + // 그 다음 다운로드 시도 + 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 serverHasData = (budgetData?.count || 0) > 0 || (transactionsData?.count || 0) > 0; + + if (!serverHasData && (backupBudgetData || backupTransactions)) { + console.log('서버에 데이터가 없고 로컬 데이터가 있어 다운로드 건너뜀'); + result.downloadSuccess = true; + result.details!.budgetDownload = true; + result.details!.transactionDownload = true; + } else { + // 예산 데이터 다운로드 + await downloadBudgets(userId); + result.details!.budgetDownload = true; + console.log('예산 다운로드 성공'); + + // 트랜잭션 데이터 다운로드 + 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')); + } + + // 부분 성공 여부 설정 + result.partial = (result.uploadSuccess || result.downloadSuccess) && !(result.uploadSuccess && result.downloadSuccess); + + // 전체 성공 여부 설정 + result.success = result.uploadSuccess || result.downloadSuccess; + + // 동기화 시간 기록 + if (result.success) { + setLastSyncTime(new Date().toISOString()); + } + + console.log('데이터 동기화 결과:', result); + return result; } catch (error) { - console.error('데이터 동기화 중 오류 발생:', error); - throw 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; + + return result; } }; + +/** + * 서버에 대한 안전한 동기화 래퍼 + * 오류 처리와 재시도 로직을 포함 + */ +export const trySyncAllData = async (userId: string): Promise => { + console.log('안전한 데이터 동기화 시도'); + let attempts = 0; + + const trySync = async (): Promise => { + try { + return await syncAllData(userId); + } catch (error) { + attempts++; + console.error(`동기화 시도 ${attempts} 실패:`, error); + + if (attempts < 2) { + console.log('동기화 재시도 중...'); + return trySync(); + } + + return { + success: false, + partial: false, + uploadSuccess: false, + downloadSuccess: false + }; + } + }; + + return trySync(); +};