문서 파일 정리
This commit is contained in:
@@ -20,6 +20,9 @@ import PaymentMethods from './pages/PaymentMethods';
|
||||
import Settings from './pages/Settings';
|
||||
import { BudgetProvider } from './contexts/BudgetContext';
|
||||
import PrivateRoute from './components/auth/PrivateRoute';
|
||||
import NetworkStatusIndicator from './components/NetworkStatusIndicator';
|
||||
import { initSyncState, startNetworkMonitoring } from './utils/syncUtils';
|
||||
|
||||
// 전역 오류 핸들러
|
||||
const handleError = (error: Error | unknown) => {
|
||||
console.error('앱 오류 발생:', error);
|
||||
@@ -79,6 +82,10 @@ function App() {
|
||||
// 웹뷰 콘텐츠가 완전히 로드되었을 때만 스플래시 화면을 숨김
|
||||
const onAppReady = async () => {
|
||||
try {
|
||||
// 네트워크 모니터링 및 동기화 상태 초기화
|
||||
await initSyncState();
|
||||
console.log('동기화 상태 초기화 완료');
|
||||
|
||||
// 스플래시 화면을 더 빠르게 숨김 (데이터 로딩과 별도로 진행)
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
@@ -145,6 +152,7 @@ function App() {
|
||||
</Routes>
|
||||
</div>
|
||||
<Toaster />
|
||||
<NetworkStatusIndicator />
|
||||
</div>
|
||||
</Router>
|
||||
</BudgetProvider>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Check, ChevronDown, ChevronUp, Wallet } from 'lucide-react';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { markBudgetAsModified } from '@/utils/sync/budget/modifiedBudgetsTracker';
|
||||
|
||||
interface BudgetGoalProps {
|
||||
initialBudgets: {
|
||||
@@ -69,6 +69,16 @@ const BudgetInputCard: React.FC<BudgetGoalProps> = ({
|
||||
// 즉시 콜랩시블을 닫아 사용자에게 완료 피드백 제공
|
||||
setIsOpen(false);
|
||||
|
||||
// 월간 예산 변경 시 수정 추적 시스템에 기록
|
||||
if (selectedTab === 'monthly') {
|
||||
try {
|
||||
markBudgetAsModified(amount);
|
||||
console.log(`[예산 추적] 월간 예산 변경 추적: ${amount}원`);
|
||||
} catch (error) {
|
||||
console.error('[예산 추적] 예산 변경 추적 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 예산 저장
|
||||
onSave(selectedTab, amount);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { EXPENSE_CATEGORIES, categoryIcons } from '@/constants/categoryIcons';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { markSingleCategoryBudgetAsModified } from '@/utils/sync/budget/modifiedBudgetsTracker';
|
||||
|
||||
interface CategoryBudgetInputsProps {
|
||||
categoryBudgets: Record<string, number>;
|
||||
@@ -27,6 +27,15 @@ const CategoryBudgetInputs: React.FC<CategoryBudgetInputsProps> = ({
|
||||
const numericValue = e.target.value.replace(/[^0-9]/g, '');
|
||||
handleCategoryInputChange(numericValue, category);
|
||||
|
||||
// 수정된 카테고리 예산 추적 시스템에 기록
|
||||
try {
|
||||
const amount = parseInt(numericValue, 10) || 0;
|
||||
markSingleCategoryBudgetAsModified(category, amount);
|
||||
console.log(`[예산 추적] 카테고리 '${category}' 예산 변경 추적: ${amount}원`);
|
||||
} catch (error) {
|
||||
console.error(`[예산 추적] 카테고리 '${category}' 예산 변경 추적 실패:`, error);
|
||||
}
|
||||
|
||||
// 사용자에게 시각적 피드백 제공
|
||||
e.target.classList.add('border-green-500');
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAuth } from '@/contexts/auth/useAuth';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import { saveTransactionsToStorage } from './storageUtils';
|
||||
import { deleteTransactionFromServer } from '@/utils/sync/transaction/deleteTransaction';
|
||||
import { addToDeletedTransactions } from '@/utils/sync/transaction/deletedTransactionsTracker';
|
||||
|
||||
/**
|
||||
* 안정화된 트랜잭션 삭제 훅 - 완전 재구현 버전
|
||||
@@ -49,6 +50,14 @@ export const useDeleteTransaction = (
|
||||
// 트랜잭션 찾기
|
||||
const updatedTransactions = transactions.filter(t => t.id !== id);
|
||||
|
||||
// 삭제된 트랜잭션 추적 목록에 추가
|
||||
try {
|
||||
addToDeletedTransactions(id);
|
||||
console.log(`[안정화] 삭제된 트랜잭션 추적 추가 (ID: ${id})`);
|
||||
} catch (trackingError) {
|
||||
console.error('[안정화] 삭제 추적 실패:', trackingError);
|
||||
}
|
||||
|
||||
// 로컬 스토리지 저장
|
||||
try {
|
||||
saveTransactionsToStorage(updatedTransactions);
|
||||
@@ -111,8 +120,10 @@ export const useDeleteTransaction = (
|
||||
|
||||
// 컴포넌트 언마운트 시 모든 상태 정리
|
||||
useEffect(() => {
|
||||
// 현재 ref 값을 로컬 변수에 복사하여 클린업 함수에서 사용
|
||||
const pendingDeletion = pendingDeletionRef.current;
|
||||
return () => {
|
||||
pendingDeletionRef.current.clear();
|
||||
pendingDeletion.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { isSyncEnabled } from '../syncSettings';
|
||||
import { getModifiedBudget, getModifiedCategoryBudgets } from './modifiedBudgetsTracker';
|
||||
|
||||
/**
|
||||
* 서버에서 예산 데이터 다운로드
|
||||
@@ -107,9 +107,12 @@ async function fetchCategoryBudgetData(userId: string) {
|
||||
/**
|
||||
* 예산 데이터 처리 및 로컬 저장
|
||||
*/
|
||||
async function processBudgetData(budgetData: any, localBudgetDataStr: string | null) {
|
||||
async function processBudgetData(budgetData: Record<string, any>, localBudgetDataStr: string | null) {
|
||||
console.log('서버에서 예산 데이터 수신:', budgetData);
|
||||
|
||||
// 로컬에서 수정된 예산 정보 가져오기
|
||||
const modifiedBudget = getModifiedBudget();
|
||||
|
||||
// 서버 예산이 0이고 로컬 예산이 있으면 로컬 데이터 유지
|
||||
if (budgetData.total_budget === 0 && localBudgetDataStr) {
|
||||
console.log('서버 예산이 0이고 로컬 예산이 있어 로컬 데이터 유지');
|
||||
@@ -117,12 +120,51 @@ async function processBudgetData(budgetData: any, localBudgetDataStr: string | n
|
||||
}
|
||||
|
||||
// 기존 로컬 데이터 가져오기
|
||||
let localBudgetData = localBudgetDataStr ? JSON.parse(localBudgetDataStr) : {
|
||||
const localBudgetData = localBudgetDataStr ? JSON.parse(localBudgetDataStr) : {
|
||||
daily: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 },
|
||||
weekly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 },
|
||||
monthly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 }
|
||||
};
|
||||
|
||||
// 로컬에서 수정된 예산이 있고, 서버 데이터보다 최신이면 로컬 데이터 유지
|
||||
if (modifiedBudget && (!budgetData.updated_at || new Date(budgetData.updated_at).getTime() < modifiedBudget.timestamp)) {
|
||||
console.log('로컬에서 수정된 예산이 서버 데이터보다 최신이므로 로컬 데이터 유지');
|
||||
|
||||
// 서버 데이터 대신 로컬에서 수정된 예산 사용
|
||||
const monthlyBudget = modifiedBudget.monthlyAmount;
|
||||
const dailyBudget = Math.round(monthlyBudget / 30); // 월간 예산 / 30일
|
||||
const weeklyBudget = Math.round(monthlyBudget / 4.3); // 월간 예산 / 4.3주
|
||||
|
||||
const updatedBudgetData = {
|
||||
daily: {
|
||||
targetAmount: dailyBudget,
|
||||
spentAmount: localBudgetData.daily.spentAmount,
|
||||
remainingAmount: dailyBudget - localBudgetData.daily.spentAmount
|
||||
},
|
||||
weekly: {
|
||||
targetAmount: weeklyBudget,
|
||||
spentAmount: localBudgetData.weekly.spentAmount,
|
||||
remainingAmount: weeklyBudget - localBudgetData.weekly.spentAmount
|
||||
},
|
||||
monthly: {
|
||||
targetAmount: monthlyBudget,
|
||||
spentAmount: localBudgetData.monthly.spentAmount,
|
||||
remainingAmount: monthlyBudget - localBudgetData.monthly.spentAmount
|
||||
}
|
||||
};
|
||||
|
||||
console.log('로컬 수정 데이터 기반 예산 계산:', updatedBudgetData);
|
||||
|
||||
// 로컬 스토리지에 저장
|
||||
localStorage.setItem('budgetData', JSON.stringify(updatedBudgetData));
|
||||
localStorage.setItem('budgetData_backup', JSON.stringify(updatedBudgetData));
|
||||
console.log('로컬 수정 예산 데이터 유지 완료', updatedBudgetData);
|
||||
|
||||
// 이벤트 발생시켜 UI 업데이트
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 서버 데이터로 업데이트 (지출 금액은 유지)
|
||||
// 수정: 올바른 예산 계산 방식으로 변경
|
||||
const monthlyBudget = budgetData.total_budget;
|
||||
@@ -161,9 +203,12 @@ async function processBudgetData(budgetData: any, localBudgetDataStr: string | n
|
||||
/**
|
||||
* 카테고리 예산 데이터 처리 및 로컬 저장
|
||||
*/
|
||||
async function processCategoryBudgetData(categoryData: any[], localCategoryBudgetsStr: string | null) {
|
||||
async function processCategoryBudgetData(categoryData: Record<string, any>[], localCategoryBudgetsStr: string | null) {
|
||||
console.log(`${categoryData.length}개의 카테고리 예산 수신`);
|
||||
|
||||
// 로컬에서 수정된 카테고리 예산 정보 가져오기
|
||||
const modifiedCategoryBudgets = getModifiedCategoryBudgets();
|
||||
|
||||
// 서버 카테고리 예산 합계 계산
|
||||
const serverTotal = categoryData.reduce((sum, item) => sum + item.amount, 0);
|
||||
|
||||
@@ -173,6 +218,21 @@ async function processCategoryBudgetData(categoryData: any[], localCategoryBudge
|
||||
return;
|
||||
}
|
||||
|
||||
// 로컬에서 수정된 카테고리 예산이 있고, 서버 데이터보다 최신이면 로컬 데이터 유지
|
||||
if (modifiedCategoryBudgets && categoryData.length > 0) {
|
||||
// 서버 데이터 중 가장 최근 업데이트 시간 확인
|
||||
const latestServerUpdate = categoryData.reduce((latest, curr) => {
|
||||
if (!curr.updated_at) return latest;
|
||||
const currTime = new Date(curr.updated_at).getTime();
|
||||
return currTime > latest ? currTime : latest;
|
||||
}, 0);
|
||||
|
||||
if (latestServerUpdate < modifiedCategoryBudgets.timestamp) {
|
||||
console.log('로컬에서 수정된 카테고리 예산이 서버 데이터보다 최신이므로 로컬 데이터 유지');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 카테고리 예산 로컬 형식으로 변환
|
||||
const localCategoryBudgets = categoryData.reduce((acc, curr) => {
|
||||
acc[curr.category] = curr.amount;
|
||||
|
||||
138
src/utils/sync/budget/modifiedBudgetsTracker.ts
Normal file
138
src/utils/sync/budget/modifiedBudgetsTracker.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 수정된 예산 데이터를 추적하는 유틸리티
|
||||
* 로컬 스토리지에 수정된 예산 정보를 저장하고 관리합니다.
|
||||
*/
|
||||
|
||||
const MODIFIED_BUDGETS_KEY = 'modified_budgets';
|
||||
const MODIFIED_CATEGORY_BUDGETS_KEY = 'modified_category_budgets';
|
||||
|
||||
interface ModifiedBudget {
|
||||
timestamp: number; // 수정 시간 (밀리초)
|
||||
monthlyAmount: number; // 월간 예산액
|
||||
}
|
||||
|
||||
interface ModifiedCategoryBudgets {
|
||||
timestamp: number; // 수정 시간 (밀리초)
|
||||
categories: Record<string, number>; // 카테고리별 예산액
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정된 예산 정보를 로컬 스토리지에 저장
|
||||
*/
|
||||
export const markBudgetAsModified = (monthlyAmount: number): void => {
|
||||
try {
|
||||
const modifiedBudget: ModifiedBudget = {
|
||||
timestamp: Date.now(),
|
||||
monthlyAmount
|
||||
};
|
||||
|
||||
localStorage.setItem(MODIFIED_BUDGETS_KEY, JSON.stringify(modifiedBudget));
|
||||
console.log(`[예산 추적] 수정된 예산 정보 저장 완료: ${monthlyAmount}원`);
|
||||
} catch (error) {
|
||||
console.error('[예산 추적] 수정된 예산 정보 저장 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 수정된 카테고리 예산 정보를 로컬 스토리지에 저장
|
||||
*/
|
||||
export const markCategoryBudgetsAsModified = (categories: Record<string, number>): void => {
|
||||
try {
|
||||
const modifiedCategoryBudgets: ModifiedCategoryBudgets = {
|
||||
timestamp: Date.now(),
|
||||
categories
|
||||
};
|
||||
|
||||
localStorage.setItem(MODIFIED_CATEGORY_BUDGETS_KEY, JSON.stringify(modifiedCategoryBudgets));
|
||||
console.log(`[예산 추적] 수정된 카테고리 예산 정보 저장 완료: ${Object.keys(categories).length}개 카테고리`);
|
||||
} catch (error) {
|
||||
console.error('[예산 추적] 수정된 카테고리 예산 정보 저장 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 단일 카테고리 예산 정보를 수정된 것으로 표시
|
||||
*/
|
||||
export const markSingleCategoryBudgetAsModified = (category: string, amount: number): void => {
|
||||
try {
|
||||
// 기존 수정 정보 가져오기
|
||||
const existing = getModifiedCategoryBudgets();
|
||||
const categories = existing?.categories || {};
|
||||
|
||||
// 새 카테고리 예산 정보 추가
|
||||
categories[category] = amount;
|
||||
|
||||
// 수정된 정보 저장
|
||||
const modifiedCategoryBudgets: ModifiedCategoryBudgets = {
|
||||
timestamp: Date.now(),
|
||||
categories
|
||||
};
|
||||
|
||||
localStorage.setItem(MODIFIED_CATEGORY_BUDGETS_KEY, JSON.stringify(modifiedCategoryBudgets));
|
||||
console.log(`[예산 추적] 카테고리 '${category}' 예산 정보 저장 완료: ${amount}원`);
|
||||
} catch (error) {
|
||||
console.error(`[예산 추적] 카테고리 '${category}' 예산 정보 저장 실패:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 수정된 예산 정보 가져오기
|
||||
*/
|
||||
export const getModifiedBudget = (): ModifiedBudget | null => {
|
||||
try {
|
||||
const data = localStorage.getItem(MODIFIED_BUDGETS_KEY);
|
||||
if (!data) return null;
|
||||
|
||||
return JSON.parse(data) as ModifiedBudget;
|
||||
} catch (error) {
|
||||
console.error('[예산 추적] 수정된 예산 정보 조회 실패:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 수정된 카테고리 예산 정보 가져오기
|
||||
*/
|
||||
export const getModifiedCategoryBudgets = (): ModifiedCategoryBudgets | null => {
|
||||
try {
|
||||
const data = localStorage.getItem(MODIFIED_CATEGORY_BUDGETS_KEY);
|
||||
if (!data) return null;
|
||||
|
||||
return JSON.parse(data) as ModifiedCategoryBudgets;
|
||||
} catch (error) {
|
||||
console.error('[예산 추적] 수정된 카테고리 예산 정보 조회 실패:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 예산 수정 정보 초기화
|
||||
*/
|
||||
export const clearModifiedBudget = (): void => {
|
||||
try {
|
||||
localStorage.removeItem(MODIFIED_BUDGETS_KEY);
|
||||
console.log('[예산 추적] 수정된 예산 정보 초기화 완료');
|
||||
} catch (error) {
|
||||
console.error('[예산 추적] 수정된 예산 정보 초기화 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 예산 수정 정보 초기화
|
||||
*/
|
||||
export const clearModifiedCategoryBudgets = (): void => {
|
||||
try {
|
||||
localStorage.removeItem(MODIFIED_CATEGORY_BUDGETS_KEY);
|
||||
console.log('[예산 추적] 수정된 카테고리 예산 정보 초기화 완료');
|
||||
} catch (error) {
|
||||
console.error('[예산 추적] 수정된 카테고리 예산 정보 초기화 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 모든 수정 정보 초기화
|
||||
*/
|
||||
export const clearAllModifiedBudgets = (): void => {
|
||||
clearModifiedBudget();
|
||||
clearModifiedCategoryBudgets();
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { isSyncEnabled } from '../syncSettings';
|
||||
import {
|
||||
clearModifiedBudget,
|
||||
clearModifiedCategoryBudgets
|
||||
} from './modifiedBudgetsTracker';
|
||||
|
||||
/**
|
||||
* 예산 데이터를 서버에 업로드
|
||||
@@ -18,6 +21,9 @@ export const uploadBudgets = async (userId: string): Promise<void> => {
|
||||
if (budgetDataStr) {
|
||||
const budgetData = JSON.parse(budgetDataStr);
|
||||
await uploadBudgetData(userId, budgetData);
|
||||
|
||||
// 업로드 성공 후 수정 추적 정보 초기화
|
||||
clearModifiedBudget();
|
||||
} else {
|
||||
console.log('업로드할 예산 데이터가 없음');
|
||||
}
|
||||
@@ -26,6 +32,9 @@ export const uploadBudgets = async (userId: string): Promise<void> => {
|
||||
if (categoryBudgetsStr) {
|
||||
const categoryBudgets = JSON.parse(categoryBudgetsStr);
|
||||
await uploadCategoryBudgets(userId, categoryBudgets);
|
||||
|
||||
// 업로드 성공 후 수정 추적 정보 초기화
|
||||
clearModifiedCategoryBudgets();
|
||||
} else {
|
||||
console.log('업로드할 카테고리 예산이 없음');
|
||||
}
|
||||
@@ -40,7 +49,7 @@ export const uploadBudgets = async (userId: string): Promise<void> => {
|
||||
/**
|
||||
* 일반 예산 데이터 업로드
|
||||
*/
|
||||
async function uploadBudgetData(userId: string, parsedBudgetData: any): Promise<void> {
|
||||
async function uploadBudgetData(userId: string, parsedBudgetData: Record<string, any>): Promise<void> {
|
||||
console.log('예산 데이터 업로드:', parsedBudgetData);
|
||||
|
||||
// 현재 월/년도 가져오기
|
||||
@@ -66,6 +75,9 @@ async function uploadBudgetData(userId: string, parsedBudgetData: any): Promise<
|
||||
|
||||
console.log('업로드할 월간 예산:', monthlyTarget);
|
||||
|
||||
// 현재 타임스탬프
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
|
||||
// 업데이트 또는 삽입 결정
|
||||
if (existingBudgets && existingBudgets.length > 0) {
|
||||
// 기존 데이터 업데이트
|
||||
@@ -73,7 +85,7 @@ async function uploadBudgetData(userId: string, parsedBudgetData: any): Promise<
|
||||
.from('budgets')
|
||||
.update({
|
||||
total_budget: monthlyTarget,
|
||||
updated_at: new Date().toISOString()
|
||||
updated_at: currentTimestamp
|
||||
})
|
||||
.eq('id', existingBudgets[0].id);
|
||||
|
||||
@@ -91,7 +103,9 @@ async function uploadBudgetData(userId: string, parsedBudgetData: any): Promise<
|
||||
user_id: userId,
|
||||
month: currentMonth,
|
||||
year: currentYear,
|
||||
total_budget: monthlyTarget
|
||||
total_budget: monthlyTarget,
|
||||
created_at: currentTimestamp,
|
||||
updated_at: currentTimestamp
|
||||
});
|
||||
|
||||
if (error) {
|
||||
@@ -120,13 +134,18 @@ async function uploadCategoryBudgets(userId: string, parsedCategoryBudgets: Reco
|
||||
// 오류가 나도 계속 진행 (중요 데이터가 아니기 때문)
|
||||
}
|
||||
|
||||
// 현재 타임스탬프
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
|
||||
// 카테고리별 예산 데이터 변환 및 삽입
|
||||
const categoryEntries = Object.entries(parsedCategoryBudgets)
|
||||
.filter(([_, amount]) => amount > 0) // 금액이 0보다 큰 것만 저장
|
||||
.map(([category, amount]) => ({
|
||||
user_id: userId,
|
||||
category,
|
||||
amount
|
||||
amount,
|
||||
created_at: currentTimestamp,
|
||||
updated_at: currentTimestamp
|
||||
}));
|
||||
|
||||
if (categoryEntries.length > 0) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { isSyncEnabled } from '../syncSettings';
|
||||
import { addToDeletedTransactions } from './deletedTransactionsTracker';
|
||||
|
||||
/**
|
||||
* Supabase 서버에서 트랜잭션을 삭제하는 함수 - 안정성 및 성능 최적화 버전
|
||||
@@ -21,6 +22,9 @@ export const deleteTransactionFromServer = async (userId: string, transactionId:
|
||||
}, 2000);
|
||||
|
||||
try {
|
||||
// 삭제된 트랜잭션 ID 추적 목록에 추가
|
||||
addToDeletedTransactions(transactionId);
|
||||
|
||||
// 서버 요청 실행
|
||||
const { error } = await supabase
|
||||
.from('transactions')
|
||||
|
||||
69
src/utils/sync/transaction/deletedTransactionsTracker.ts
Normal file
69
src/utils/sync/transaction/deletedTransactionsTracker.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 삭제된 트랜잭션 ID를 추적하는 유틸리티
|
||||
* 로컬에서 삭제된 트랜잭션이 서버 동기화 후 다시 나타나는 문제를 해결합니다.
|
||||
*/
|
||||
|
||||
// 삭제된 트랜잭션 ID를 저장하는 로컬 스토리지 키
|
||||
const DELETED_TRANSACTIONS_KEY = 'deletedTransactions';
|
||||
|
||||
/**
|
||||
* 삭제된 트랜잭션 ID를 저장
|
||||
* @param id 삭제된 트랜잭션 ID
|
||||
*/
|
||||
export const addToDeletedTransactions = (id: string): void => {
|
||||
try {
|
||||
const deletedIds = getDeletedTransactions();
|
||||
if (!deletedIds.includes(id)) {
|
||||
deletedIds.push(id);
|
||||
localStorage.setItem(DELETED_TRANSACTIONS_KEY, JSON.stringify(deletedIds));
|
||||
console.log(`[삭제 추적] ID 추가됨: ${id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[삭제 추적] ID 추가 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 삭제된 트랜잭션 ID 목록 가져오기
|
||||
* @returns 삭제된 트랜잭션 ID 배열
|
||||
*/
|
||||
export const getDeletedTransactions = (): string[] => {
|
||||
try {
|
||||
const deletedStr = localStorage.getItem(DELETED_TRANSACTIONS_KEY);
|
||||
const deletedIds = deletedStr ? JSON.parse(deletedStr) : [];
|
||||
return Array.isArray(deletedIds) ? deletedIds : [];
|
||||
} catch (error) {
|
||||
console.error('[삭제 추적] 목록 조회 실패:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 삭제된 트랜잭션 ID 제거 (서버에서 성공적으로 삭제된 경우)
|
||||
* @param id 제거할 트랜잭션 ID
|
||||
*/
|
||||
export const removeFromDeletedTransactions = (id: string): void => {
|
||||
try {
|
||||
const deletedIds = getDeletedTransactions();
|
||||
const updatedIds = deletedIds.filter(deletedId => deletedId !== id);
|
||||
|
||||
if (deletedIds.length !== updatedIds.length) {
|
||||
localStorage.setItem(DELETED_TRANSACTIONS_KEY, JSON.stringify(updatedIds));
|
||||
console.log(`[삭제 추적] ID 제거됨: ${id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[삭제 추적] ID 제거 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 삭제된 트랜잭션 ID 목록 초기화
|
||||
*/
|
||||
export const clearDeletedTransactions = (): void => {
|
||||
try {
|
||||
localStorage.removeItem(DELETED_TRANSACTIONS_KEY);
|
||||
console.log('[삭제 추적] 목록 초기화됨');
|
||||
} catch (error) {
|
||||
console.error('[삭제 추적] 목록 초기화 실패:', error);
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { supabase } from '@/lib/supabase';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { isSyncEnabled } from '../syncSettings';
|
||||
import { formatDateForDisplay } from './dateUtils';
|
||||
import { getDeletedTransactions } from './deletedTransactionsTracker';
|
||||
|
||||
/**
|
||||
* Download transaction data from Supabase to local storage
|
||||
@@ -30,40 +31,53 @@ export const downloadTransactions = async (userId: string): Promise<void> => {
|
||||
|
||||
console.log(`서버에서 ${data.length}개의 트랜잭션 다운로드`);
|
||||
|
||||
// 서버 데이터를 로컬 형식으로 변환
|
||||
const serverTransactions = data.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(); // 잘못된 날짜는 현재 날짜로 대체
|
||||
}
|
||||
}
|
||||
formattedDate = formatDateForDisplay(t.date);
|
||||
// 삭제된 트랜잭션 ID 목록 가져오기
|
||||
const deletedIds = getDeletedTransactions();
|
||||
console.log(`[동기화] 삭제된 트랜잭션 ${deletedIds.length}개 필터링 적용`);
|
||||
|
||||
// 서버 데이터를 로컬 형식으로 변환 (삭제된 항목 제외)
|
||||
const serverTransactions = data
|
||||
.filter(t => {
|
||||
const transactionId = t.transaction_id || t.id;
|
||||
const isDeleted = deletedIds.includes(transactionId);
|
||||
if (isDeleted) {
|
||||
console.log(`[동기화] 삭제된 트랜잭션 필터링: ${transactionId}`);
|
||||
}
|
||||
} 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,
|
||||
date: formattedDate,
|
||||
category: t.category,
|
||||
type: t.type,
|
||||
notes: t.notes
|
||||
};
|
||||
});
|
||||
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(); // 잘못된 날짜는 현재 날짜로 대체
|
||||
}
|
||||
}
|
||||
formattedDate = formatDateForDisplay(t.date);
|
||||
}
|
||||
} 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,
|
||||
date: formattedDate,
|
||||
category: t.category,
|
||||
type: t.type,
|
||||
notes: t.notes
|
||||
};
|
||||
});
|
||||
|
||||
// 기존 로컬 데이터 불러오기
|
||||
const localDataStr = localStorage.getItem('transactions');
|
||||
|
||||
Reference in New Issue
Block a user