Refactor code based on feedback
Refactor the code based on the provided feedback.
This commit is contained in:
@@ -1,23 +1,380 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React, { createContext, useState, useContext, useEffect, useCallback } from 'react';
|
||||||
import { useBudgetState } from './useBudgetState';
|
import { Transaction, BudgetData } from './types';
|
||||||
import { BudgetContext, BudgetContextType } from './useBudget';
|
import {
|
||||||
import { BudgetPeriod, Transaction } from './types';
|
safelyLoadBudgetData,
|
||||||
|
calculateSpentAmounts,
|
||||||
|
DEFAULT_MONTHLY_BUDGET
|
||||||
|
} from './budgetUtils';
|
||||||
|
import {
|
||||||
|
loadBudgetDataFromStorage,
|
||||||
|
saveBudgetDataToStorage
|
||||||
|
} from './storage';
|
||||||
|
import { loadCategoryBudgetsFromStorage, saveCategoryBudgetsToStorage } from './storage';
|
||||||
|
import { loadTransactionsFromStorage, saveTransactionsToStorage } from './storage';
|
||||||
|
import { useCategoryBudgetState } from './hooks/useCategoryBudgetState';
|
||||||
|
import { useBudgetDataEvents } from './hooks/useBudgetDataEvents';
|
||||||
|
import { useBudgetDataLoad } from './hooks/useBudgetDataLoad';
|
||||||
|
import { toast } from '@/hooks/useToast.wrapper';
|
||||||
|
import { APP_EVENTS } from '@/utils/eventEmitter';
|
||||||
|
|
||||||
// 컨텍스트 프로바이더 컴포넌트
|
// 컨텍스트 타입 정의
|
||||||
|
interface BudgetContextType {
|
||||||
|
transactions: Transaction[];
|
||||||
|
filteredTransactions: Transaction[];
|
||||||
|
budgetData: BudgetData;
|
||||||
|
categoryBudgets: Record<string, number>;
|
||||||
|
selectedTab: string;
|
||||||
|
isInitialized: boolean;
|
||||||
|
lastUpdateTime: number;
|
||||||
|
|
||||||
|
setSelectedTab: (tab: string) => void;
|
||||||
|
addTransaction: (transaction: Transaction) => void;
|
||||||
|
updateTransaction: (updatedTransaction: Transaction) => void;
|
||||||
|
deleteTransaction: (transactionId: string) => void;
|
||||||
|
handleBudgetGoalUpdate: (targetAmount: number) => void;
|
||||||
|
getCategorySpending: (category: string, transactions?: Transaction[]) => number;
|
||||||
|
updateCategoryBudgets: (newCategoryBudgets: Record<string, number>) => void;
|
||||||
|
resetBudgetData: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본값으로 컨텍스트 생성
|
||||||
|
const BudgetContext = createContext<BudgetContextType>({
|
||||||
|
transactions: [],
|
||||||
|
filteredTransactions: [],
|
||||||
|
budgetData: {
|
||||||
|
daily: {
|
||||||
|
targetAmount: 0,
|
||||||
|
spentAmount: 0,
|
||||||
|
remainingAmount: 0
|
||||||
|
},
|
||||||
|
weekly: {
|
||||||
|
targetAmount: 0,
|
||||||
|
spentAmount: 0,
|
||||||
|
remainingAmount: 0
|
||||||
|
},
|
||||||
|
monthly: {
|
||||||
|
targetAmount: 0,
|
||||||
|
spentAmount: 0,
|
||||||
|
remainingAmount: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
categoryBudgets: {},
|
||||||
|
selectedTab: 'monthly',
|
||||||
|
isInitialized: false,
|
||||||
|
lastUpdateTime: 0,
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
setSelectedTab: () => {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
addTransaction: () => {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
updateTransaction: () => {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
deleteTransaction: () => {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
handleBudgetGoalUpdate: () => {},
|
||||||
|
getCategorySpending: () => 0,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
updateCategoryBudgets: () => {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
resetBudgetData: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컨텍스트 사용 훅
|
||||||
|
export const useBudget = () => useContext(BudgetContext);
|
||||||
|
|
||||||
|
// 컨텍스트 제공자 컴포넌트
|
||||||
export const BudgetProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const BudgetProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const budgetState = useBudgetState();
|
// 상태 정의
|
||||||
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
|
const [filteredTransactions, setFilteredTransactions] = useState<Transaction[]>([]);
|
||||||
|
const [budgetData, setBudgetData] = useState<BudgetData>(safelyLoadBudgetData());
|
||||||
|
const [selectedTab, setSelectedTab] = useState('monthly');
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
const [lastUpdateTime, setLastUpdateTime] = useState(Date.now());
|
||||||
|
|
||||||
|
// 카테고리 예산 상태 관리
|
||||||
|
const {
|
||||||
|
categoryBudgets,
|
||||||
|
updateCategoryBudgets
|
||||||
|
} = useCategoryBudgetState();
|
||||||
|
|
||||||
|
// 예산 데이터 로드 및 초기화
|
||||||
|
const { loadBudgetData } = useBudgetDataLoad(
|
||||||
|
isInitialized,
|
||||||
|
setIsInitialized,
|
||||||
|
budgetData,
|
||||||
|
setBudgetData,
|
||||||
|
setLastUpdateTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// 예산 데이터 이벤트 처리
|
||||||
|
useBudgetDataEvents(
|
||||||
|
isInitialized,
|
||||||
|
transactions,
|
||||||
|
setBudgetData,
|
||||||
|
setLastUpdateTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// 첫 마운트 시 데이터 로드
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
console.log('BudgetContext 초기화 중...');
|
||||||
|
|
||||||
|
// 트랜잭션 데이터 로드 (성능 최적화: 마운트 시 한 번만)
|
||||||
|
const loadedTransactions = loadTransactionsFromStorage();
|
||||||
|
setTransactions(loadedTransactions);
|
||||||
|
|
||||||
|
// 필터링 없이 초기 데이터 설정
|
||||||
|
setFilteredTransactions(loadedTransactions);
|
||||||
|
|
||||||
|
// 카테고리 예산 및 예산 데이터는 해당 훅에서 처리
|
||||||
|
|
||||||
|
// 이벤트 리스너 설정
|
||||||
|
const handleTransactionUpdate = () => {
|
||||||
|
console.log('트랜잭션 업데이트 이벤트 발생');
|
||||||
|
const updatedTransactions = loadTransactionsFromStorage();
|
||||||
|
setTransactions(updatedTransactions);
|
||||||
|
setFilteredTransactions(updatedTransactions);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 예산이 업데이트될 때마다 콘솔 출력
|
||||||
|
const handleBudgetUpdate = () => {
|
||||||
|
console.log('BudgetContext: 전역 예산 데이터 이벤트 감지, 현재 targetAmount=' + budgetData.monthly.targetAmount);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 이벤트 리스너 등록
|
||||||
|
window.addEventListener('transactionUpdated', handleTransactionUpdate);
|
||||||
|
window.addEventListener('budgetDataUpdated', handleBudgetUpdate);
|
||||||
|
window.addEventListener(APP_EVENTS.TRANSACTION_UPDATED, handleTransactionUpdate);
|
||||||
|
window.addEventListener(APP_EVENTS.DATA_UPDATED, handleTransactionUpdate);
|
||||||
|
|
||||||
|
console.log('BudgetContext 초기화 완료');
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('transactionUpdated', handleTransactionUpdate);
|
||||||
|
window.removeEventListener('budgetDataUpdated', handleBudgetUpdate);
|
||||||
|
window.removeEventListener(APP_EVENTS.TRANSACTION_UPDATED, handleTransactionUpdate);
|
||||||
|
window.removeEventListener(APP_EVENTS.DATA_UPDATED, handleTransactionUpdate);
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('BudgetContext 초기화 오류:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 현재 예산 데이터 업데이트 로깅 (디버깅용)
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(`최신 예산 데이터: ${JSON.stringify(budgetData)} 마지막 업데이트: ${new Date(lastUpdateTime).toISOString()}`);
|
||||||
|
console.log(`예산 상태 업데이트: 트랜잭션 수: ${transactions.length} 카테고리 예산: ${JSON.stringify(categoryBudgets)} 예산 데이터: ${JSON.stringify(budgetData)}`);
|
||||||
|
}, [budgetData, transactions.length, categoryBudgets, lastUpdateTime]);
|
||||||
|
|
||||||
|
// 예산 목표 업데이트 핸들러
|
||||||
|
const handleBudgetGoalUpdate = useCallback((targetAmount: number) => {
|
||||||
|
console.log(`예산 목표 업데이트 요청: ${targetAmount}원`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 현재 예산 데이터 복사
|
||||||
|
const updatedBudgetData = { ...budgetData };
|
||||||
|
|
||||||
|
// 월간 예산 설정
|
||||||
|
updatedBudgetData.monthly.targetAmount = targetAmount;
|
||||||
|
updatedBudgetData.monthly.remainingAmount = targetAmount - updatedBudgetData.monthly.spentAmount;
|
||||||
|
|
||||||
|
// 일일 예산 계산 (월간 ÷ 30)
|
||||||
|
const dailyBudget = Math.floor(targetAmount / 30);
|
||||||
|
updatedBudgetData.daily.targetAmount = dailyBudget;
|
||||||
|
updatedBudgetData.daily.remainingAmount = dailyBudget - updatedBudgetData.daily.spentAmount;
|
||||||
|
|
||||||
|
// 주간 예산 계산 (월간 ÷ 4.3)
|
||||||
|
const weeklyBudget = Math.floor(targetAmount / 4.3);
|
||||||
|
updatedBudgetData.weekly.targetAmount = weeklyBudget;
|
||||||
|
updatedBudgetData.weekly.remainingAmount = weeklyBudget - updatedBudgetData.weekly.spentAmount;
|
||||||
|
|
||||||
|
// 새로운 상태로 업데이트
|
||||||
|
setBudgetData(updatedBudgetData);
|
||||||
|
|
||||||
|
// 저장소에도 저장
|
||||||
|
saveBudgetDataToStorage(updatedBudgetData);
|
||||||
|
|
||||||
|
// 마지막 업데이트 시간 갱신
|
||||||
|
setLastUpdateTime(Date.now());
|
||||||
|
|
||||||
|
console.log(`예산 목표 업데이트 완료: ${targetAmount}원`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('예산 목표 업데이트 중 오류:', error);
|
||||||
|
toast({
|
||||||
|
title: "예산 업데이트 실패",
|
||||||
|
description: "예산 목표를 업데이트하는데 문제가 발생했습니다.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [budgetData]);
|
||||||
|
|
||||||
|
// 트랜잭션 추가 함수
|
||||||
|
const addTransaction = useCallback((transaction: Transaction) => {
|
||||||
|
try {
|
||||||
|
const updatedTransactions = [transaction, ...transactions];
|
||||||
|
setTransactions(updatedTransactions);
|
||||||
|
setFilteredTransactions(updatedTransactions);
|
||||||
|
|
||||||
|
// 로컬 스토리지에 저장
|
||||||
|
saveTransactionsToStorage(updatedTransactions);
|
||||||
|
|
||||||
|
console.log(`트랜잭션 추가 완료: ${transaction.title}, ${transaction.amount}원`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('트랜잭션 추가 중 오류:', error);
|
||||||
|
toast({
|
||||||
|
title: "지출 추가 실패",
|
||||||
|
description: "지출 내역을 추가하는데 문제가 발생했습니다.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [transactions]);
|
||||||
|
|
||||||
|
// 트랜잭션 업데이트 함수
|
||||||
|
const updateTransaction = useCallback((updatedTransaction: Transaction) => {
|
||||||
|
try {
|
||||||
|
const updatedTransactions = transactions.map(transaction =>
|
||||||
|
transaction.id === updatedTransaction.id ? updatedTransaction : transaction
|
||||||
|
);
|
||||||
|
|
||||||
|
setTransactions(updatedTransactions);
|
||||||
|
setFilteredTransactions(updatedTransactions);
|
||||||
|
|
||||||
|
// 로컬 스토리지에 저장
|
||||||
|
saveTransactionsToStorage(updatedTransactions);
|
||||||
|
|
||||||
|
console.log(`트랜잭션 업데이트 완료: ${updatedTransaction.title}, ${updatedTransaction.amount}원`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('트랜잭션 업데이트 중 오류:', error);
|
||||||
|
toast({
|
||||||
|
title: "지출 업데이트 실패",
|
||||||
|
description: "지출 내역을 업데이트하는데 문제가 발생했습니다.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [transactions]);
|
||||||
|
|
||||||
|
// 트랜잭션 삭제 함수
|
||||||
|
const deleteTransaction = useCallback((transactionId: string) => {
|
||||||
|
try {
|
||||||
|
const updatedTransactions = transactions.filter(
|
||||||
|
transaction => transaction.id !== transactionId
|
||||||
|
);
|
||||||
|
|
||||||
|
setTransactions(updatedTransactions);
|
||||||
|
setFilteredTransactions(updatedTransactions);
|
||||||
|
|
||||||
|
// 로컬 스토리지에 저장
|
||||||
|
saveTransactionsToStorage(updatedTransactions);
|
||||||
|
|
||||||
|
console.log(`트랜잭션 삭제 완료: ID ${transactionId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('트랜잭션 삭제 중 오류:', error);
|
||||||
|
toast({
|
||||||
|
title: "지출 삭제 실패",
|
||||||
|
description: "지출 내역을 삭제하는데 문제가 발생했습니다.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [transactions]);
|
||||||
|
|
||||||
|
// 예산 데이터 초기화 함수
|
||||||
|
const resetBudgetData = useCallback(() => {
|
||||||
|
try {
|
||||||
|
console.log('예산 데이터 초기화 시작');
|
||||||
|
|
||||||
|
// 기본 예산 데이터로 초기화
|
||||||
|
const defaultBudgetData: BudgetData = {
|
||||||
|
daily: {
|
||||||
|
targetAmount: Math.floor(DEFAULT_MONTHLY_BUDGET / 30),
|
||||||
|
spentAmount: 0,
|
||||||
|
remainingAmount: Math.floor(DEFAULT_MONTHLY_BUDGET / 30)
|
||||||
|
},
|
||||||
|
weekly: {
|
||||||
|
targetAmount: Math.floor(DEFAULT_MONTHLY_BUDGET / 4.3),
|
||||||
|
spentAmount: 0,
|
||||||
|
remainingAmount: Math.floor(DEFAULT_MONTHLY_BUDGET / 4.3)
|
||||||
|
},
|
||||||
|
monthly: {
|
||||||
|
targetAmount: DEFAULT_MONTHLY_BUDGET,
|
||||||
|
spentAmount: 0,
|
||||||
|
remainingAmount: DEFAULT_MONTHLY_BUDGET
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 상태 업데이트
|
||||||
|
setBudgetData(defaultBudgetData);
|
||||||
|
|
||||||
|
// 저장소에 저장
|
||||||
|
saveBudgetDataToStorage(defaultBudgetData);
|
||||||
|
|
||||||
|
// 마지막 업데이트 시간 갱신
|
||||||
|
setLastUpdateTime(Date.now());
|
||||||
|
|
||||||
|
console.log('예산 데이터 초기화 완료');
|
||||||
|
toast({
|
||||||
|
title: "예산 초기화 완료",
|
||||||
|
description: "모든 예산 데이터가 기본값으로 초기화되었습니다.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('예산 데이터 초기화 중 오류:', error);
|
||||||
|
toast({
|
||||||
|
title: "예산 초기화 실패",
|
||||||
|
description: "예산 데이터를 초기화하는데 문제가 발생했습니다.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 카테고리별 지출 금액 계산 함수
|
||||||
|
const getCategorySpending = useCallback((category: string, txs?: Transaction[]): number => {
|
||||||
|
try {
|
||||||
|
const transactionsToUse = txs || transactions;
|
||||||
|
|
||||||
|
// 특정 카테고리의 지출 항목만 필터링
|
||||||
|
const categoryTransactions = transactionsToUse.filter(
|
||||||
|
t => t.category === category && t.type === 'expense'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 지출 금액 합계 계산
|
||||||
|
const totalSpent = categoryTransactions.reduce(
|
||||||
|
(sum, transaction) => sum + transaction.amount,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return totalSpent;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`카테고리 지출 계산 중 오류 (${category}):`, error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}, [transactions]);
|
||||||
|
|
||||||
|
// 컨텍스트 값 정의
|
||||||
|
const contextValue: BudgetContextType = {
|
||||||
|
transactions,
|
||||||
|
filteredTransactions,
|
||||||
|
budgetData,
|
||||||
|
categoryBudgets,
|
||||||
|
selectedTab,
|
||||||
|
isInitialized,
|
||||||
|
lastUpdateTime,
|
||||||
|
|
||||||
|
setSelectedTab,
|
||||||
|
addTransaction,
|
||||||
|
updateTransaction,
|
||||||
|
deleteTransaction,
|
||||||
|
handleBudgetGoalUpdate,
|
||||||
|
getCategorySpending,
|
||||||
|
updateCategoryBudgets,
|
||||||
|
resetBudgetData
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컨텍스트 제공
|
||||||
return (
|
return (
|
||||||
<BudgetContext.Provider value={budgetState}>
|
<BudgetContext.Provider value={contextValue}>
|
||||||
{children}
|
{children}
|
||||||
</BudgetContext.Provider>
|
</BudgetContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// useBudget 훅은 useBudget.ts 파일로 이동했습니다
|
|
||||||
export { useBudget } from './useBudget';
|
|
||||||
export type { BudgetContextType } from './useBudget';
|
|
||||||
|
|
||||||
// types.ts에서 타입들을 export type으로 내보냅니다
|
|
||||||
export type { BudgetPeriod, Transaction } from './types';
|
|
||||||
|
|||||||
105
src/hooks/useOptimizedDataSync.ts
Normal file
105
src/hooks/useOptimizedDataSync.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* 최적화된 데이터 동기화 훅
|
||||||
|
*/
|
||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { useAuth } from '@/contexts/auth';
|
||||||
|
import { optimizedSync, debouncedSync, throttledSync } from '@/utils/sync/syncOptimizer';
|
||||||
|
import { isSyncEnabled } from '@/utils/syncUtils';
|
||||||
|
import { emitEvent, APP_EVENTS } from '@/utils/eventEmitter';
|
||||||
|
|
||||||
|
export function useOptimizedDataSync() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
|
const [lastSyncTime, setLastSyncTime] = useState<string | null>(
|
||||||
|
localStorage.getItem('lastSyncTime')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 동기화 상태 추적
|
||||||
|
useEffect(() => {
|
||||||
|
// 동기화 완료 이벤트 리스너
|
||||||
|
const handleSyncComplete = (e: CustomEvent) => {
|
||||||
|
setIsSyncing(false);
|
||||||
|
|
||||||
|
// 성공 시 마지막 동기화 시간 업데이트
|
||||||
|
if (e.detail?.success) {
|
||||||
|
const time = new Date().toISOString();
|
||||||
|
setLastSyncTime(time);
|
||||||
|
localStorage.setItem('lastSyncTime', time);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 동기화 시작 이벤트 리스너
|
||||||
|
const handleSyncStart = () => {
|
||||||
|
setIsSyncing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 이벤트 리스너 등록
|
||||||
|
window.addEventListener(APP_EVENTS.SYNC_STARTED, handleSyncStart as EventListener);
|
||||||
|
window.addEventListener(APP_EVENTS.SYNC_COMPLETED, handleSyncComplete as EventListener);
|
||||||
|
window.addEventListener(APP_EVENTS.SYNC_FAILED, () => setIsSyncing(false));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// 이벤트 리스너 제거
|
||||||
|
window.removeEventListener(APP_EVENTS.SYNC_STARTED, handleSyncStart as EventListener);
|
||||||
|
window.removeEventListener(APP_EVENTS.SYNC_COMPLETED, handleSyncComplete as EventListener);
|
||||||
|
window.removeEventListener(APP_EVENTS.SYNC_FAILED, () => setIsSyncing(false));
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 즉시 동기화 실행 함수
|
||||||
|
const syncNow = useCallback(async () => {
|
||||||
|
if (!user || !isSyncEnabled() || isSyncing) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 동기화 시작 이벤트 발생
|
||||||
|
emitEvent(APP_EVENTS.SYNC_STARTED);
|
||||||
|
|
||||||
|
// 동기화 실행
|
||||||
|
const result = await optimizedSync(user.id);
|
||||||
|
|
||||||
|
// 동기화 완료 이벤트 발생
|
||||||
|
emitEvent(APP_EVENTS.SYNC_COMPLETED, { success: result.success, data: result });
|
||||||
|
|
||||||
|
return result.success;
|
||||||
|
} catch (error) {
|
||||||
|
// 동기화 실패 이벤트 발생
|
||||||
|
emitEvent(APP_EVENTS.SYNC_FAILED, { error });
|
||||||
|
console.error('[동기화] 오류:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [user, isSyncing]);
|
||||||
|
|
||||||
|
// 자동 동기화 (페이지 로드 시)
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && isSyncEnabled()) {
|
||||||
|
// 페이지 로드 시 한 번 실행 (스로틀 적용)
|
||||||
|
const syncOnLoad = async () => {
|
||||||
|
await throttledSync(user.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 페이지가 완전히 로드된 후 동기화 실행
|
||||||
|
const timer = setTimeout(syncOnLoad, 1000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// 디바운스된 동기화 함수
|
||||||
|
const debouncedSyncNow = useCallback(() => {
|
||||||
|
if (!user || !isSyncEnabled()) return;
|
||||||
|
|
||||||
|
// 동기화 시작 이벤트 발생 (UI 업데이트용)
|
||||||
|
emitEvent(APP_EVENTS.SYNC_STARTED);
|
||||||
|
|
||||||
|
// 디바운스된 동기화 실행
|
||||||
|
debouncedSync(user.id);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSyncing,
|
||||||
|
lastSyncTime,
|
||||||
|
syncNow,
|
||||||
|
debouncedSyncNow
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ import { useWelcomeDialog } from '@/hooks/useWelcomeDialog';
|
|||||||
import { useDataInitialization } from '@/hooks/useDataInitialization';
|
import { useDataInitialization } from '@/hooks/useDataInitialization';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import useNotifications from '@/hooks/useNotifications';
|
import useNotifications from '@/hooks/useNotifications';
|
||||||
|
import { setupPageVisibilityEvents, emitEvent, APP_EVENTS } from '@/utils/eventEmitter';
|
||||||
|
import { useOptimizedDataSync } from '@/hooks/useOptimizedDataSync';
|
||||||
|
import SafeAreaContainer from '@/components/SafeAreaContainer';
|
||||||
|
|
||||||
// 메인 컴포넌트
|
// 메인 컴포넌트
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
@@ -30,6 +33,12 @@ const Index = () => {
|
|||||||
const { isInitialized } = useDataInitialization(resetBudgetData);
|
const { isInitialized } = useDataInitialization(resetBudgetData);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { addNotification } = useNotifications();
|
const { addNotification } = useNotifications();
|
||||||
|
const { syncNow } = useOptimizedDataSync();
|
||||||
|
|
||||||
|
// 페이지 가시성 이벤트 설정 (최적화)
|
||||||
|
useEffect(() => {
|
||||||
|
setupPageVisibilityEvents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 초기화 후 환영 메시지 표시 상태 확인
|
// 초기화 후 환영 메시지 표시 상태 확인
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,98 +68,45 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
}, [isInitialized, user, addNotification]);
|
}, [isInitialized, user, addNotification]);
|
||||||
|
|
||||||
// 페이지가 처음 로드될 때 데이터 로딩 확인
|
// 페이지가 처음 로드될 때 데이터 로딩 최적화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Index 페이지 마운트, 현재 데이터 상태:');
|
console.log('Index 페이지 마운트');
|
||||||
console.log('트랜잭션:', transactions.length);
|
|
||||||
console.log('예산 데이터:', budgetData);
|
|
||||||
|
|
||||||
// 페이지 마운트 시 데이터 동기화 이벤트 수동 발생
|
// 페이지 로드 시 한 번에 모든 데이터 이벤트 발생 (통합 처리)
|
||||||
|
emitEvent(APP_EVENTS.PAGE_LOADED);
|
||||||
|
|
||||||
|
// 백업된 데이터 확인 작업은 마운트 시 한 번만 수행
|
||||||
try {
|
try {
|
||||||
window.dispatchEvent(new Event('transactionUpdated'));
|
// 데이터 존재 여부 확인 및 백업 복구 (한 번만 실행)
|
||||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
if (!localStorage.getItem('budgetData') && localStorage.getItem('budgetData_backup')) {
|
||||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
localStorage.setItem('budgetData', localStorage.getItem('budgetData_backup')!);
|
||||||
} catch (e) {
|
emitEvent(APP_EVENTS.BUDGET_UPDATED);
|
||||||
console.error('이벤트 발생 오류:', e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 백업된 데이터 복구 확인 (메인 데이터가 없는 경우만)
|
if (!localStorage.getItem('categoryBudgets') && localStorage.getItem('categoryBudgets_backup')) {
|
||||||
try {
|
localStorage.setItem('categoryBudgets', localStorage.getItem('categoryBudgets_backup')!);
|
||||||
if (!localStorage.getItem('budgetData')) {
|
emitEvent(APP_EVENTS.CATEGORY_UPDATED);
|
||||||
const budgetBackup = localStorage.getItem('budgetData_backup');
|
|
||||||
if (budgetBackup) {
|
|
||||||
console.log('예산 데이터 백업에서 복구');
|
|
||||||
localStorage.setItem('budgetData', budgetBackup);
|
|
||||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!localStorage.getItem('categoryBudgets')) {
|
if (!localStorage.getItem('transactions') && localStorage.getItem('transactions_backup')) {
|
||||||
const categoryBackup = localStorage.getItem('categoryBudgets_backup');
|
localStorage.setItem('transactions', localStorage.getItem('transactions_backup')!);
|
||||||
if (categoryBackup) {
|
emitEvent(APP_EVENTS.TRANSACTION_UPDATED);
|
||||||
console.log('카테고리 예산 백업에서 복구');
|
|
||||||
localStorage.setItem('categoryBudgets', categoryBackup);
|
|
||||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!localStorage.getItem('transactions')) {
|
// 로그인 상태면 동기화 수행 (지연 시작으로 초기 로딩 성능 개선)
|
||||||
const transactionBackup = localStorage.getItem('transactions_backup');
|
if (user) {
|
||||||
if (transactionBackup) {
|
setTimeout(() => {
|
||||||
console.log('트랜잭션 백업에서 복구');
|
syncNow();
|
||||||
localStorage.setItem('transactions', transactionBackup);
|
}, 1500);
|
||||||
window.dispatchEvent(new Event('transactionUpdated'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('백업 복구 시도 중 오류:', error);
|
console.error('백업 복구 시도 중 오류:', error);
|
||||||
}
|
}
|
||||||
}, [transactions.length, budgetData]);
|
|
||||||
|
|
||||||
// 앱이 포커스를 얻었을 때 데이터를 새로고침
|
|
||||||
useEffect(() => {
|
|
||||||
const handleFocus = () => {
|
|
||||||
console.log('창이 포커스를 얻음 - 데이터 새로고침');
|
|
||||||
// 이벤트 발생시켜 데이터 새로고침
|
|
||||||
try {
|
|
||||||
window.dispatchEvent(new Event('storage'));
|
|
||||||
window.dispatchEvent(new Event('transactionUpdated'));
|
|
||||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
|
||||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('이벤트 발생 오류:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 포커스 이벤트
|
|
||||||
window.addEventListener('focus', handleFocus);
|
|
||||||
|
|
||||||
// 가시성 변경 이벤트 (백그라운드에서 전경으로 돌아올 때)
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
console.log('페이지가 다시 보임 - 데이터 새로고침');
|
|
||||||
handleFocus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 정기적인 데이터 새로고침 (10초마다)
|
|
||||||
const refreshInterval = setInterval(() => {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
console.log('정기 새로고침 - 데이터 업데이트');
|
|
||||||
handleFocus();
|
|
||||||
}
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('focus', handleFocus);
|
|
||||||
document.removeEventListener('visibilitychange', () => {});
|
|
||||||
clearInterval(refreshInterval);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neuro-background pb-24">
|
<SafeAreaContainer className="min-h-screen bg-neuro-background">
|
||||||
<div className="max-w-md mx-auto px-6">
|
<div className="max-w-md mx-auto px-6 pb-24">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<HomeContent
|
<HomeContent
|
||||||
@@ -168,7 +124,7 @@ const Index = () => {
|
|||||||
|
|
||||||
{/* 첫 사용자 안내 팝업 */}
|
{/* 첫 사용자 안내 팝업 */}
|
||||||
<WelcomeDialog open={showWelcome} onClose={handleCloseWelcome} />
|
<WelcomeDialog open={showWelcome} onClose={handleCloseWelcome} />
|
||||||
</div>
|
</SafeAreaContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
142
src/utils/eventEmitter.ts
Normal file
142
src/utils/eventEmitter.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 버스 최적화 - 통합된 이벤트 관리 시스템
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 전역 이벤트 이름 정의
|
||||||
|
export const APP_EVENTS = {
|
||||||
|
// 데이터 변경 이벤트
|
||||||
|
DATA_UPDATED: 'app:data-updated',
|
||||||
|
TRANSACTION_UPDATED: 'app:transaction-updated',
|
||||||
|
BUDGET_UPDATED: 'app:budget-updated',
|
||||||
|
CATEGORY_UPDATED: 'app:category-updated',
|
||||||
|
|
||||||
|
// 인증 이벤트
|
||||||
|
AUTH_STATE_CHANGED: 'app:auth-state-changed',
|
||||||
|
USER_LOGGED_IN: 'app:user-logged-in',
|
||||||
|
USER_LOGGED_OUT: 'app:user-logged-out',
|
||||||
|
|
||||||
|
// 동기화 이벤트
|
||||||
|
SYNC_STARTED: 'app:sync-started',
|
||||||
|
SYNC_COMPLETED: 'app:sync-completed',
|
||||||
|
SYNC_FAILED: 'app:sync-failed',
|
||||||
|
|
||||||
|
// UI 이벤트
|
||||||
|
PAGE_LOADED: 'app:page-loaded',
|
||||||
|
TAB_CHANGED: 'app:tab-changed',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최적화된 이벤트 발생 함수 (디바운스, 이벤트 통합 등)
|
||||||
|
let queuedEvents: Record<string, any> = {};
|
||||||
|
let eventQueueTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 발생 - 디바운스 적용 (짧은 시간 내 동일 이벤트는 마지막 한 번만 발생)
|
||||||
|
*/
|
||||||
|
export function emitEvent(eventName: string, detail?: any): void {
|
||||||
|
// 이벤트 대기열에 저장
|
||||||
|
queuedEvents[eventName] = { detail };
|
||||||
|
|
||||||
|
// 이미 타이머가 설정되어 있으면 기존 타이머 유지
|
||||||
|
if (eventQueueTimer) return;
|
||||||
|
|
||||||
|
// 타이머 설정 (10ms 후 대기열의 이벤트 모두 발생)
|
||||||
|
eventQueueTimer = setTimeout(() => {
|
||||||
|
// 대기열의 모든 이벤트 처리
|
||||||
|
for (const [name, data] of Object.entries(queuedEvents)) {
|
||||||
|
try {
|
||||||
|
// 표준 이벤트 발생
|
||||||
|
window.dispatchEvent(new CustomEvent(name, {
|
||||||
|
detail: data.detail,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 하위 호환성 유지를 위한 기존 이벤트 매핑
|
||||||
|
if (name === APP_EVENTS.TRANSACTION_UPDATED) {
|
||||||
|
window.dispatchEvent(new Event('transactionUpdated'));
|
||||||
|
} else if (name === APP_EVENTS.BUDGET_UPDATED) {
|
||||||
|
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||||
|
} else if (name === APP_EVENTS.CATEGORY_UPDATED) {
|
||||||
|
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[이벤트] 발생: ${name}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[이벤트] 발생 오류 (${name}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대기열 및 타이머 초기화
|
||||||
|
queuedEvents = {};
|
||||||
|
eventQueueTimer = null;
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 여러 이벤트 한 번에 발생 (배치 이벤트)
|
||||||
|
*/
|
||||||
|
export function emitBatchEvents(events: { name: string; detail?: any }[]): void {
|
||||||
|
// 모든 이벤트를 대기열에 추가
|
||||||
|
events.forEach(event => {
|
||||||
|
queuedEvents[event.name] = { detail: event.detail };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 기존 타이머 취소
|
||||||
|
if (eventQueueTimer) {
|
||||||
|
clearTimeout(eventQueueTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새 타이머 설정 (즉시 실행)
|
||||||
|
eventQueueTimer = setTimeout(() => {
|
||||||
|
// 대기열의 모든 이벤트 처리
|
||||||
|
for (const [name, data] of Object.entries(queuedEvents)) {
|
||||||
|
try {
|
||||||
|
// 표준 이벤트 발생
|
||||||
|
window.dispatchEvent(new CustomEvent(name, {
|
||||||
|
detail: data.detail,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 하위 호환성 유지를 위한 기존 이벤트 매핑
|
||||||
|
if (name === APP_EVENTS.TRANSACTION_UPDATED) {
|
||||||
|
window.dispatchEvent(new Event('transactionUpdated'));
|
||||||
|
} else if (name === APP_EVENTS.BUDGET_UPDATED) {
|
||||||
|
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||||
|
} else if (name === APP_EVENTS.CATEGORY_UPDATED) {
|
||||||
|
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[이벤트] 배치 발생: ${name}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[이벤트] 배치 발생 오류 (${name}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대기열 및 타이머 초기화
|
||||||
|
queuedEvents = {};
|
||||||
|
eventQueueTimer = null;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 페이지가 포커스를 얻었을 때 전체 데이터 새로고침 이벤트 발생
|
||||||
|
*/
|
||||||
|
export function setupPageVisibilityEvents(): void {
|
||||||
|
// 포커스 이벤트
|
||||||
|
window.addEventListener('focus', () => {
|
||||||
|
console.log('[이벤트] 창이 포커스를 얻음');
|
||||||
|
emitEvent(APP_EVENTS.PAGE_LOADED);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 가시성 변경 이벤트
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
console.log('[이벤트] 페이지가 다시 보임');
|
||||||
|
emitEvent(APP_EVENTS.PAGE_LOADED);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[이벤트] 페이지 가시성 이벤트 설정 완료');
|
||||||
|
}
|
||||||
118
src/utils/performance/debounceThrottle.ts
Normal file
118
src/utils/performance/debounceThrottle.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* 성능 최적화를 위한 유틸리티 함수
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 디바운스: 여러 호출 중 마지막 호출만 실행
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
return function (...args: Parameters<T>) {
|
||||||
|
const later = () => {
|
||||||
|
timeout = null;
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스로틀: 일정 시간 내에 한 번만 실행
|
||||||
|
export function throttle<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
limit: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let inThrottle = false;
|
||||||
|
|
||||||
|
return function (...args: Parameters<T>) {
|
||||||
|
if (!inThrottle) {
|
||||||
|
func(...args);
|
||||||
|
inThrottle = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
inThrottle = false;
|
||||||
|
}, limit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실행 중인 작업 추적
|
||||||
|
const pendingOperations: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
// 중복 실행 방지 (한 번에 동일 작업 한 번만 실행)
|
||||||
|
export function preventDuplicateOperation<T extends (...args: any[]) => Promise<any>>(
|
||||||
|
operationKey: string,
|
||||||
|
func: T
|
||||||
|
): (...args: Parameters<T>) => Promise<ReturnType<T> | void> {
|
||||||
|
return async function (...args: Parameters<T>): Promise<ReturnType<T> | void> {
|
||||||
|
// 이미 실행 중인 작업이면 중단
|
||||||
|
if (pendingOperations[operationKey]) {
|
||||||
|
console.log(`[성능] ${operationKey} 작업이 이미 실행 중입니다. 중복 요청 무시.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 작업 시작 플래그 설정
|
||||||
|
pendingOperations[operationKey] = true;
|
||||||
|
console.log(`[성능] ${operationKey} 작업 시작`);
|
||||||
|
|
||||||
|
// 작업 실행
|
||||||
|
const result = await func(...args);
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
// 작업 종료 후 플래그 해제
|
||||||
|
pendingOperations[operationKey] = false;
|
||||||
|
console.log(`[성능] ${operationKey} 작업 완료`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 네트워크 요청 캐시 (메모리 캐시, 세션 내 유효)
|
||||||
|
const requestCache: Record<string, { data: any; timestamp: number }> = {};
|
||||||
|
|
||||||
|
// 캐시 설정 (TTL: 캐시 유효 시간(ms))
|
||||||
|
export function withCache<T extends (...args: any[]) => Promise<any>>(
|
||||||
|
cacheKey: string,
|
||||||
|
func: T,
|
||||||
|
ttl: number = 60000 // 기본값 1분
|
||||||
|
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
|
||||||
|
return async function (...args: Parameters<T>): Promise<ReturnType<T>> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 캐시 유효성 확인
|
||||||
|
if (
|
||||||
|
requestCache[cacheKey] &&
|
||||||
|
now - requestCache[cacheKey].timestamp < ttl
|
||||||
|
) {
|
||||||
|
console.log(`[성능] 캐시된 데이터 사용: ${cacheKey}`);
|
||||||
|
return requestCache[cacheKey].data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐시 만료 또는 없음 - 새로 요청
|
||||||
|
console.log(`[성능] 새 데이터 요청: ${cacheKey}`);
|
||||||
|
const result = await func(...args);
|
||||||
|
|
||||||
|
// 결과 캐싱
|
||||||
|
requestCache[cacheKey] = {
|
||||||
|
data: result,
|
||||||
|
timestamp: now
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 배치 상태 업데이트 관리
|
||||||
|
export function batchedUpdates<T>(updates: () => T): T {
|
||||||
|
// React의 unstable_batchedUpdates 사용 (가능한 경우)
|
||||||
|
if (typeof window !== 'undefined' && 'ReactDOM' in window) {
|
||||||
|
// @ts-ignore: ReactDOM이 전역에 존재할 수 있음
|
||||||
|
return window.ReactDOM.unstable_batchedUpdates(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 폴백: 기본 실행
|
||||||
|
return updates();
|
||||||
|
}
|
||||||
99
src/utils/sync/syncOptimizer.ts
Normal file
99
src/utils/sync/syncOptimizer.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* 동기화 최적화 유틸리티
|
||||||
|
*/
|
||||||
|
import { debounce, throttle, preventDuplicateOperation } from '../performance/debounceThrottle';
|
||||||
|
import { trySyncAllData } from '@/utils/syncUtils';
|
||||||
|
import { toast } from '@/hooks/useToast.wrapper';
|
||||||
|
|
||||||
|
// 동기화 상태 플래그
|
||||||
|
let isSyncRunning = false;
|
||||||
|
let lastSyncTime = 0;
|
||||||
|
const MIN_SYNC_INTERVAL = 30000; // 최소 동기화 간격 (30초)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 최적화된 동기화 함수 - 중복 방지, 속도 제한 적용
|
||||||
|
*/
|
||||||
|
export const optimizedSync = preventDuplicateOperation(
|
||||||
|
'sync-all-data',
|
||||||
|
async (userId: string) => {
|
||||||
|
// 이미 동기화 중이면 중단
|
||||||
|
if (isSyncRunning) {
|
||||||
|
console.log('[최적화] 이미 동기화 작업이 진행 중입니다.');
|
||||||
|
return { success: false, reason: 'already-running' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 너무 빈번한 동기화 요청 방지
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastSyncTime < MIN_SYNC_INTERVAL) {
|
||||||
|
console.log(`[최적화] 동기화 요청이 너무 빈번합니다. ${MIN_SYNC_INTERVAL / 1000}초 후 다시 시도하세요.`);
|
||||||
|
return { success: false, reason: 'too-frequent' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isSyncRunning = true;
|
||||||
|
console.log('[최적화] 동기화 시작...');
|
||||||
|
|
||||||
|
// 네트워크 상태 확인
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
console.log('[최적화] 오프라인 상태입니다. 동기화를 건너뜁니다.');
|
||||||
|
return { success: false, reason: 'offline' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실제 동기화 수행
|
||||||
|
const result = await trySyncAllData(userId);
|
||||||
|
|
||||||
|
// 마지막 동기화 시간 기록
|
||||||
|
lastSyncTime = Date.now();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[최적화] 동기화 오류:', error);
|
||||||
|
|
||||||
|
// 중요 오류만 사용자에게 표시
|
||||||
|
toast({
|
||||||
|
title: "동기화 오류",
|
||||||
|
description: "데이터 동기화 중 문제가 발생했습니다. 나중에 다시 시도해주세요.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: false, reason: 'error', error };
|
||||||
|
} finally {
|
||||||
|
isSyncRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 디바운스된 동기화 함수 (빠르게 여러 번 호출해도 마지막 한 번만 실행)
|
||||||
|
*/
|
||||||
|
export const debouncedSync = debounce(
|
||||||
|
async (userId: string) => {
|
||||||
|
console.log('[최적화] 디바운스된 동기화 실행');
|
||||||
|
return optimizedSync(userId);
|
||||||
|
},
|
||||||
|
2000 // 2초 대기
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스로틀된 동기화 함수 (일정 시간 내 한 번만 실행)
|
||||||
|
*/
|
||||||
|
export const throttledSync = throttle(
|
||||||
|
async (userId: string) => {
|
||||||
|
console.log('[최적화] 스로틀된 동기화 실행');
|
||||||
|
return optimizedSync(userId);
|
||||||
|
},
|
||||||
|
10000 // 10초마다 최대 한 번
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 자동 동기화 시도 (에러 무시)
|
||||||
|
*/
|
||||||
|
export const attemptBackgroundSync = async (userId: string) => {
|
||||||
|
try {
|
||||||
|
return await optimizedSync(userId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[최적화] 백그라운드 동기화 오류 (무시됨):', error);
|
||||||
|
return { success: false, reason: 'background-error' };
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user