Fix data loss on sync after reset

Addresses an issue where budget data was lost after a data reset, logout, and subsequent synchronization. The fix ensures budget data is correctly restored in such scenarios.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-21 12:29:28 +00:00
parent f7eb7d5af7
commit befb29611b
4 changed files with 303 additions and 138 deletions

View File

@@ -1,47 +1,49 @@
import { SyncResult } from '@/utils/sync/data';
import { toast } from '@/hooks/useToast.wrapper'; import { toast } from '@/hooks/useToast.wrapper';
import type { SyncResult } from '@/utils/syncUtils';
/** // 동기화 결과 처리 함수
* 동기화 결과 처리 함수
*/
export const handleSyncResult = (result: SyncResult) => { export const handleSyncResult = (result: SyncResult) => {
if (!result) {
toast({
title: "동기화 오류",
description: "동기화 결과를 처리할 수 없습니다.",
variant: "destructive"
});
return;
}
if (result.success) { if (result.success) {
if (result.partial) { if (result.uploadSuccess && result.downloadSuccess) {
// 부분 성공 처리 // 양방향 동기화 성공
toast({
title: "부분 동기화 완료",
description: "일부 데이터만 동기화되었습니다. 다시 시도해보세요.",
variant: "default"
});
} else {
// 전체 성공 처리
toast({ toast({
title: "동기화 완료", title: "동기화 완료",
description: "모든 데이터가 성공적으로 동기화되었습니다.", description: "모든 데이터가 성공적으로 동기화되었습니다.",
variant: "default"
}); });
} else if (result.uploadSuccess) {
// 데이터 변경 이벤트 발생 // 업로드만 성공
window.dispatchEvent(new Event('budgetDataUpdated')); toast({
window.dispatchEvent(new Event('categoryBudgetsUpdated')); title: "동기화 완료",
window.dispatchEvent(new Event('transactionUpdated')); description: "로컬 데이터가 클라우드에 업로드되었습니다.",
});
} else if (result.downloadSuccess) {
// 다운로드만 성공
toast({
title: "동기화 완료",
description: "클라우드 데이터가 기기에 다운로드되었습니다.",
});
} }
// 상세 결과 로깅
console.log("동기화 세부 결과:", {
예산업로드: result.details?.budgetUpload ? '성공' : '실패',
예산다운로드: result.details?.budgetDownload ? '성공' : '실패',
트랜잭션업로드: result.details?.transactionUpload ? '성공' : '실패',
트랜잭션다운로드: result.details?.transactionDownload ? '성공' : '실패'
});
return true;
} else { } else {
// 실패 처리 // 동기화 실패
console.error("동기화 실패 세부 결과:", result.details);
toast({ toast({
title: "동기화 실패", title: "동기화 실패",
description: "데이터 동기화에 실패했습니다. 나중에 다시 시도해세요.", description: "데이터 동기화 중 문제가 발생했습니다. 다시 시도해세요.",
variant: "destructive" variant: "destructive"
}); });
return false;
} }
}; };

View File

@@ -1,3 +1,4 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { isSyncEnabled } from '../syncSettings'; import { isSyncEnabled } from '../syncSettings';
import { import {
@@ -20,10 +21,16 @@ export const uploadBudgets = async (userId: string): Promise<void> => {
// 예산 데이터 업로드 // 예산 데이터 업로드
if (budgetDataStr) { if (budgetDataStr) {
const budgetData = JSON.parse(budgetDataStr); const budgetData = JSON.parse(budgetDataStr);
await uploadBudgetData(userId, budgetData);
// 업로드 성공 후 수정 추적 정보 초기화 // 월간 예산이 0보다 클 때만 업로드
clearModifiedBudget(); if (budgetData.monthly && budgetData.monthly.targetAmount > 0) {
console.log('유효한 월간 예산 발견:', budgetData.monthly.targetAmount);
await uploadBudgetData(userId, budgetData);
// 업로드 성공 후 수정 추적 정보 초기화
clearModifiedBudget();
} else {
console.log('월간 예산이 0 이하거나 없어서 업로드 건너뜀');
}
} else { } else {
console.log('업로드할 예산 데이터가 없음'); console.log('업로드할 예산 데이터가 없음');
} }
@@ -31,10 +38,17 @@ export const uploadBudgets = async (userId: string): Promise<void> => {
// 카테고리 예산 업로드 // 카테고리 예산 업로드
if (categoryBudgetsStr) { if (categoryBudgetsStr) {
const categoryBudgets = JSON.parse(categoryBudgetsStr); const categoryBudgets = JSON.parse(categoryBudgetsStr);
await uploadCategoryBudgets(userId, categoryBudgets);
// 업로드 성공 후 수정 추적 정보 초기화 // 총 카테고리 예산이 0보다 클 때만 업로드
clearModifiedCategoryBudgets(); const totalCategoryBudget = Object.values(categoryBudgets).reduce((sum: number, val: number) => sum + val, 0);
if (totalCategoryBudget > 0) {
console.log('유효한 카테고리 예산 발견:', totalCategoryBudget);
await uploadCategoryBudgets(userId, categoryBudgets);
// 업로드 성공 후 수정 추적 정보 초기화
clearModifiedCategoryBudgets();
} else {
console.log('카테고리 예산이 모두 0이어서 업로드 건너뜀');
}
} else { } else {
console.log('업로드할 카테고리 예산이 없음'); console.log('업로드할 카테고리 예산이 없음');
} }
@@ -73,28 +87,42 @@ async function uploadBudgetData(userId: string, parsedBudgetData: Record<string,
// 월간 타겟 금액 가져오기 // 월간 타겟 금액 가져오기
const monthlyTarget = parsedBudgetData.monthly.targetAmount; const monthlyTarget = parsedBudgetData.monthly.targetAmount;
// 예산이 0 이하면 업로드하지 않음
if (monthlyTarget <= 0) {
console.log('월간 예산이 0 이하여서 업로드 건너뜀:', monthlyTarget);
return;
}
console.log('업로드할 월간 예산:', monthlyTarget); console.log('업로드할 월간 예산:', monthlyTarget);
// 현재 타임스탬프 // 현재 타임스탬프
const currentTimestamp = new Date().toISOString(); const currentTimestamp = new Date().toISOString();
// 업데이트 또는 삽입 결정 // 가능한 경우 서버 데이터와 비교하여 필요한 경우만 업데이트
if (existingBudgets && existingBudgets.length > 0) { if (existingBudgets && existingBudgets.length > 0) {
// 기존 데이터 업데이트 const existingBudget = existingBudgets[0];
const { error } = await supabase // 새 예산이 기존 예산보다 클 때만 업데이트
.from('budgets') if (monthlyTarget > existingBudget.total_budget) {
.update({ console.log(`새 예산(${monthlyTarget})이 기존 예산(${existingBudget.total_budget})보다 큼, 업데이트 실행`);
total_budget: monthlyTarget,
updated_at: currentTimestamp
})
.eq('id', existingBudgets[0].id);
if (error) { // 기존 데이터 업데이트
console.error('예산 데이터 업데이트 실패:', error); const { error } = await supabase
throw error; .from('budgets')
.update({
total_budget: monthlyTarget,
updated_at: currentTimestamp
})
.eq('id', existingBudget.id);
if (error) {
console.error('예산 데이터 업데이트 실패:', error);
throw error;
}
console.log('예산 데이터 업데이트 성공');
} else {
console.log(`새 예산(${monthlyTarget})이 기존 예산(${existingBudget.total_budget})보다 작거나 같음, 업데이트 건너뜀`);
} }
console.log('예산 데이터 업데이트 성공');
} else { } else {
// 새 데이터 삽입 // 새 데이터 삽입
const { error } = await supabase const { error } = await supabase
@@ -123,6 +151,40 @@ async function uploadBudgetData(userId: string, parsedBudgetData: Record<string,
async function uploadCategoryBudgets(userId: string, parsedCategoryBudgets: Record<string, number>): Promise<void> { async function uploadCategoryBudgets(userId: string, parsedCategoryBudgets: Record<string, number>): Promise<void> {
console.log('카테고리 예산 업로드:', parsedCategoryBudgets); console.log('카테고리 예산 업로드:', parsedCategoryBudgets);
// 기존 카테고리 예산 확인
const { data: existingCategoryBudgets, error: fetchError } = await supabase
.from('category_budgets')
.select('*')
.eq('user_id', userId);
if (fetchError) {
console.error('기존 카테고리 예산 조회 실패:', fetchError);
throw fetchError;
}
// 기존 카테고리 예산의 총액 계산
let existingTotal = 0;
if (existingCategoryBudgets && existingCategoryBudgets.length > 0) {
existingTotal = existingCategoryBudgets.reduce((sum, item) => sum + item.amount, 0);
}
// 새 카테고리 예산의 총액 계산
const newTotal = Object.values(parsedCategoryBudgets).reduce((sum: number, val: number) => sum + val, 0);
// 새 카테고리 예산 총액이 기존 카테고리 예산 총액보다 작거나 같으면 업로드 건너뜀
if (newTotal <= existingTotal && existingTotal > 0) {
console.log(`새 카테고리 예산 총액(${newTotal})이 기존 예산 총액(${existingTotal})보다 작거나 같음, 업로드 건너뜀`);
return;
}
// 새 카테고리 예산 총액이 0이면 업로드 건너뜀
if (newTotal <= 0) {
console.log('새 카테고리 예산 총액이 0이하여서 업로드 건너뜀');
return;
}
console.log(`새 카테고리 예산 총액(${newTotal})이 기존 예산 총액(${existingTotal})보다 큼, 업로드 실행`);
// 기존 카테고리 예산 삭제 // 기존 카테고리 예산 삭제
const { error: deleteError } = await supabase const { error: deleteError } = await supabase
.from('category_budgets') .from('category_budgets')

View File

@@ -40,81 +40,143 @@ export const syncAllData = async (userId: string): Promise<SyncResult> => {
try { try {
console.log('데이터 동기화 시작 - 사용자 ID:', userId); console.log('데이터 동기화 시작 - 사용자 ID:', userId);
// 여기서는 업로드를 먼저 시도합니다 (로컬 데이터 보존을 위해) // 서버에 데이터가 있는지 먼저 확인
try { const { data: budgetData } = await supabase
// 예산 데이터 업로드 .from('budgets')
await uploadBudgets(userId); .select('count')
result.details!.budgetUpload = true; .eq('user_id', userId)
console.log('예산 업로드 성공'); .single();
// 트랜잭션 데이터 업로드 const { data: transactionsData } = await supabase
await uploadTransactions(userId); .from('transactions')
result.details!.transactionUpload = true; .select('count')
console.log('트랜잭션 업로드 성공'); .eq('user_id', userId)
.single();
// 업로드 성공 설정 const serverHasBudgetData = (budgetData?.count || 0) > 0;
result.uploadSuccess = true; const serverHasTransactionData = (transactionsData?.count || 0) > 0;
} catch (uploadError) { const serverHasData = serverHasBudgetData || serverHasTransactionData;
console.error('데이터 업로드 실패:', uploadError);
result.uploadSuccess = false; // 로컬 데이터 존재 여부 확인
const localHasBudgetData = backupBudgetData && JSON.parse(backupBudgetData).monthly.targetAmount > 0;
const localHasTransactionData = backupTransactions && JSON.parse(backupTransactions).length > 0;
const localHasData = localHasBudgetData || localHasTransactionData;
console.log('데이터 존재 여부:', {
: {
예산: serverHasBudgetData,
거래: serverHasTransactionData
},
: {
예산: localHasBudgetData,
거래: localHasTransactionData
}
});
// 서버에 데이터가 없고 로컬에 데이터가 있으면 업로드 먼저
if (!serverHasData && localHasData) {
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;
}
} }
// 서버에 데이터가 있으면 다운로드 먼저 (기존 로직)
else if (serverHasData) {
console.log('서버에 데이터가 있어 다운로드 우선 실행');
// 그 다음 다운로드 시도 try {
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); if (serverHasBudgetData) {
result.details!.budgetDownload = true; await downloadBudgets(userId);
console.log('예산 다운로드 성공'); result.details!.budgetDownload = true;
console.log('예산 다운로드 성공');
}
// 트랜잭션 데이터 다운로드 // 트랜잭션 데이터 다운로드
await downloadTransactions(userId); if (serverHasTransactionData) {
result.details!.transactionDownload = true; await downloadTransactions(userId);
console.log('트랜잭션 다운로드 성공'); result.details!.transactionDownload = true;
console.log('트랜잭션 다운로드 성공');
}
// 다운로드 성공 설정 // 다운로드 성공 설정
result.downloadSuccess = true; result.downloadSuccess = true;
} } catch (downloadError) {
} catch (downloadError) { console.error('데이터 다운로드 실패:', downloadError);
console.error('데이터 다운로드 실패:', downloadError); result.downloadSuccess = false;
result.downloadSuccess = false;
// 다운로드 실패 시 로컬 데이터 복원 // 다운로드 실패 시 로컬 데이터 복원
if (backupBudgetData) { if (backupBudgetData) {
localStorage.setItem('budgetData', backupBudgetData); localStorage.setItem('budgetData', backupBudgetData);
} }
if (backupCategoryBudgets) { if (backupCategoryBudgets) {
localStorage.setItem('categoryBudgets', backupCategoryBudgets); localStorage.setItem('categoryBudgets', backupCategoryBudgets);
} }
if (backupTransactions) { if (backupTransactions) {
localStorage.setItem('transactions', backupTransactions); localStorage.setItem('transactions', backupTransactions);
}
// UI 업데이트
window.dispatchEvent(new Event('budgetDataUpdated'));
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
window.dispatchEvent(new Event('transactionUpdated'));
} }
// UI 업데이트 // 다운로드 후 로컬 데이터 있으면 추가 업로드 시도
window.dispatchEvent(new Event('budgetDataUpdated')); if (result.downloadSuccess && localHasData) {
window.dispatchEvent(new Event('categoryBudgetsUpdated')); console.log('다운로드 후 로컬 데이터 업로드 시도');
window.dispatchEvent(new Event('transactionUpdated'));
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;
}
}
}
// 양쪽 다 데이터가 없는 경우
else {
console.log('서버와 로컬 모두 데이터가 없어 동기화 건너뜀');
result.uploadSuccess = true;
result.downloadSuccess = true;
result.details!.budgetUpload = true;
result.details!.budgetDownload = true;
result.details!.transactionUpload = true;
result.details!.transactionDownload = true;
} }
// 부분 성공 여부 설정 // 부분 성공 여부 설정

View File

@@ -35,6 +35,12 @@ export const downloadBudgets = async (userId: string): Promise<void> => {
if (budgetData && budgetData.length > 0) { if (budgetData && budgetData.length > 0) {
const monthlyBudget = budgetData[0].total_budget; const monthlyBudget = budgetData[0].total_budget;
// 예산이 0이거나 음수면 처리하지 않음
if (monthlyBudget <= 0) {
console.log('서버에 저장된 예산이 0이하여서 다운로드 건너뜀:', monthlyBudget);
return;
}
// 기존 예산 데이터 가져오기 // 기존 예산 데이터 가져오기
let budgetDataObj = { let budgetDataObj = {
daily: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }, daily: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 },
@@ -61,26 +67,35 @@ export const downloadBudgets = async (userId: string): Promise<void> => {
console.error('로컬 예산 데이터 파싱 오류:', e); console.error('로컬 예산 데이터 파싱 오류:', e);
} }
// 월간 예산만 업데이트 // 서버의 예산이 로컬 예산보다 클 때에만 로컬 예산 덮어쓰기
budgetDataObj.monthly.targetAmount = monthlyBudget; if (monthlyBudget > budgetDataObj.monthly.targetAmount) {
console.log('서버 예산이 로컬 예산보다 큼. 로컬 예산 업데이트:',
`서버: ${monthlyBudget}, 로컬: ${budgetDataObj.monthly.targetAmount}`);
// 일간, 주간 예산 계산 (이전 비율 유지) // 월간 예산만 업데이트
const daysInMonth = new Date(currentYear, currentMonth, 0).getDate(); budgetDataObj.monthly.targetAmount = monthlyBudget;
budgetDataObj.daily = {
targetAmount: Math.round(monthlyBudget / daysInMonth),
spentAmount: budgetDataObj.daily?.spentAmount || 0,
remainingAmount: Math.round(monthlyBudget / daysInMonth) - (budgetDataObj.daily?.spentAmount || 0)
};
budgetDataObj.weekly = { // 일간, 주간 예산 계산 (이전 비율 유지)
targetAmount: Math.round(monthlyBudget * 7 / daysInMonth), const daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
spentAmount: budgetDataObj.weekly?.spentAmount || 0, budgetDataObj.daily = {
remainingAmount: Math.round(monthlyBudget * 7 / daysInMonth) - (budgetDataObj.weekly?.spentAmount || 0) targetAmount: Math.round(monthlyBudget / daysInMonth),
}; spentAmount: budgetDataObj.daily?.spentAmount || 0,
remainingAmount: Math.round(monthlyBudget / daysInMonth) - (budgetDataObj.daily?.spentAmount || 0)
};
// 업데이트된 예산 데이터 저장 budgetDataObj.weekly = {
localStorage.setItem('budgetData', JSON.stringify(budgetDataObj)); targetAmount: Math.round(monthlyBudget * 7 / daysInMonth),
console.log('월간 예산 다운로드 및 저장 완료:', monthlyBudget); spentAmount: budgetDataObj.weekly?.spentAmount || 0,
remainingAmount: Math.round(monthlyBudget * 7 / daysInMonth) - (budgetDataObj.weekly?.spentAmount || 0)
};
// 업데이트된 예산 데이터 저장
localStorage.setItem('budgetData', JSON.stringify(budgetDataObj));
console.log('월간 예산 다운로드 및 저장 완료:', monthlyBudget);
} else {
console.log('로컬 예산이 서버 예산보다 크거나 같음. 예산 유지:',
`로컬: ${budgetDataObj.monthly.targetAmount}, 서버: ${monthlyBudget}`);
}
} else { } else {
console.log('서버에 저장된 월간 예산 데이터가 없습니다.'); console.log('서버에 저장된 월간 예산 데이터가 없습니다.');
} }
@@ -97,16 +112,40 @@ export const downloadBudgets = async (userId: string): Promise<void> => {
} }
if (categoryData && categoryData.length > 0) { if (categoryData && categoryData.length > 0) {
// 현재 로컬 카테고리 예산 확인
let currentCategoryBudgets: Record<string, number> = {};
try {
const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
if (storedCategoryBudgets) {
currentCategoryBudgets = JSON.parse(storedCategoryBudgets);
}
} catch (e) {
console.error('로컬 카테고리 예산 파싱 오류:', e);
}
// 카테고리 예산 객체 구성 // 카테고리 예산 객체 구성
const categoryBudgets: Record<string, number> = {}; const serverCategoryBudgets: Record<string, number> = {};
categoryData.forEach(item => { categoryData.forEach(item => {
categoryBudgets[item.category] = item.amount; serverCategoryBudgets[item.category] = item.amount;
}); });
// 카테고리 예산 저장 // 서버 카테고리 예산 총액 계산
localStorage.setItem('categoryBudgets', JSON.stringify(categoryBudgets)); const serverTotal = Object.values(serverCategoryBudgets).reduce((sum, val) => sum + val, 0);
console.log('카테고리 예산 다운로드 및 저장 완료:', categoryBudgets); const localTotal = Object.values(currentCategoryBudgets).reduce((sum, val) => sum + val, 0);
// 서버 카테고리 예산이 로컬보다 클 때만 업데이트
if (serverTotal > localTotal) {
console.log('서버 카테고리 예산이 로컬보다 큼. 로컬 업데이트:',
`서버: ${serverTotal}, 로컬: ${localTotal}`);
// 카테고리 예산 저장
localStorage.setItem('categoryBudgets', JSON.stringify(serverCategoryBudgets));
console.log('카테고리 예산 다운로드 및 저장 완료:', serverCategoryBudgets);
} else {
console.log('로컬 카테고리 예산이 서버보다 크거나 같음. 유지:',
`로컬: ${localTotal}, 서버: ${serverTotal}`);
}
} else { } else {
console.log('서버에 저장된 카테고리 예산 데이터가 없습니다.'); console.log('서버에 저장된 카테고리 예산 데이터가 없습니다.');
} }