문서 파일 정리

This commit is contained in:
hansoo
2025-03-21 16:08:43 +09:00
parent 86c0035561
commit 2d08a7962b
64 changed files with 8460 additions and 45 deletions

View File

@@ -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>

View File

@@ -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);
};

View File

@@ -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(() => {

View File

@@ -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();
};
}, []);

View File

@@ -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;

View 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();
};

View File

@@ -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) {

View File

@@ -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')

View 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);
}
};

View File

@@ -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');