diff --git a/src/contexts/budget/BudgetContext.tsx b/src/contexts/budget/BudgetContext.tsx index 1db5361..30ca899 100644 --- a/src/contexts/budget/BudgetContext.tsx +++ b/src/contexts/budget/BudgetContext.tsx @@ -1,380 +1,23 @@ -import React, { createContext, useState, useContext, useEffect, useCallback } from 'react'; -import { Transaction, BudgetData } from './types'; -import { - 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'; +import React from 'react'; +import { useBudgetState } from './useBudgetState'; +import { BudgetContext, BudgetContextType } from './useBudget'; +import { BudgetPeriod, Transaction } from './types'; -// 컨텍스트 타입 정의 -interface BudgetContextType { - transactions: Transaction[]; - filteredTransactions: Transaction[]; - budgetData: BudgetData; - categoryBudgets: Record; - 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) => void; - resetBudgetData: () => void; -} - -// 기본값으로 컨텍스트 생성 -const BudgetContext = createContext({ - 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 }) => { - // 상태 정의 - const [transactions, setTransactions] = useState([]); - const [filteredTransactions, setFilteredTransactions] = useState([]); - const [budgetData, setBudgetData] = useState(safelyLoadBudgetData()); - const [selectedTab, setSelectedTab] = useState('monthly'); - const [isInitialized, setIsInitialized] = useState(false); - const [lastUpdateTime, setLastUpdateTime] = useState(Date.now()); + const budgetState = useBudgetState(); - // 카테고리 예산 상태 관리 - 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 ( - + {children} ); }; + +// useBudget 훅은 useBudget.ts 파일로 이동했습니다 +export { useBudget } from './useBudget'; +export type { BudgetContextType } from './useBudget'; + +// types.ts에서 타입들을 export type으로 내보냅니다 +export type { BudgetPeriod, Transaction } from './types'; diff --git a/src/hooks/useOptimizedDataSync.ts b/src/hooks/useOptimizedDataSync.ts deleted file mode 100644 index 379c460..0000000 --- a/src/hooks/useOptimizedDataSync.ts +++ /dev/null @@ -1,105 +0,0 @@ - -/** - * 최적화된 데이터 동기화 훅 - */ -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( - 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 - }; -} diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index fa6e417..8f62b5f 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -11,9 +11,6 @@ import { useWelcomeDialog } from '@/hooks/useWelcomeDialog'; import { useDataInitialization } from '@/hooks/useDataInitialization'; import { useIsMobile } from '@/hooks/use-mobile'; 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 = () => { @@ -33,12 +30,6 @@ const Index = () => { const { isInitialized } = useDataInitialization(resetBudgetData); const isMobile = useIsMobile(); const { addNotification } = useNotifications(); - const { syncNow } = useOptimizedDataSync(); - - // 페이지 가시성 이벤트 설정 (최적화) - useEffect(() => { - setupPageVisibilityEvents(); - }, []); // 초기화 후 환영 메시지 표시 상태 확인 useEffect(() => { @@ -68,45 +59,98 @@ const Index = () => { } }, [isInitialized, user, addNotification]); - // 페이지가 처음 로드될 때 데이터 로딩 최적화 + // 페이지가 처음 로드될 때 데이터 로딩 확인 useEffect(() => { - console.log('Index 페이지 마운트'); + console.log('Index 페이지 마운트, 현재 데이터 상태:'); + console.log('트랜잭션:', transactions.length); + console.log('예산 데이터:', budgetData); - // 페이지 로드 시 한 번에 모든 데이터 이벤트 발생 (통합 처리) - emitEvent(APP_EVENTS.PAGE_LOADED); - - // 백업된 데이터 확인 작업은 마운트 시 한 번만 수행 + // 페이지 마운트 시 데이터 동기화 이벤트 수동 발생 try { - // 데이터 존재 여부 확인 및 백업 복구 (한 번만 실행) - if (!localStorage.getItem('budgetData') && localStorage.getItem('budgetData_backup')) { - localStorage.setItem('budgetData', localStorage.getItem('budgetData_backup')!); - emitEvent(APP_EVENTS.BUDGET_UPDATED); + window.dispatchEvent(new Event('transactionUpdated')); + window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event('categoryBudgetsUpdated')); + } catch (e) { + console.error('이벤트 발생 오류:', e); + } + + // 백업된 데이터 복구 확인 (메인 데이터가 없는 경우만) + try { + if (!localStorage.getItem('budgetData')) { + const budgetBackup = localStorage.getItem('budgetData_backup'); + if (budgetBackup) { + console.log('예산 데이터 백업에서 복구'); + localStorage.setItem('budgetData', budgetBackup); + window.dispatchEvent(new Event('budgetDataUpdated')); + } } - if (!localStorage.getItem('categoryBudgets') && localStorage.getItem('categoryBudgets_backup')) { - localStorage.setItem('categoryBudgets', localStorage.getItem('categoryBudgets_backup')!); - emitEvent(APP_EVENTS.CATEGORY_UPDATED); + if (!localStorage.getItem('categoryBudgets')) { + const categoryBackup = localStorage.getItem('categoryBudgets_backup'); + if (categoryBackup) { + console.log('카테고리 예산 백업에서 복구'); + localStorage.setItem('categoryBudgets', categoryBackup); + window.dispatchEvent(new Event('categoryBudgetsUpdated')); + } } - if (!localStorage.getItem('transactions') && localStorage.getItem('transactions_backup')) { - localStorage.setItem('transactions', localStorage.getItem('transactions_backup')!); - emitEvent(APP_EVENTS.TRANSACTION_UPDATED); - } - - // 로그인 상태면 동기화 수행 (지연 시작으로 초기 로딩 성능 개선) - if (user) { - setTimeout(() => { - syncNow(); - }, 1500); + if (!localStorage.getItem('transactions')) { + const transactionBackup = localStorage.getItem('transactions_backup'); + if (transactionBackup) { + console.log('트랜잭션 백업에서 복구'); + localStorage.setItem('transactions', transactionBackup); + window.dispatchEvent(new Event('transactionUpdated')); + } } } catch (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 ( - -
+
+
{ {/* 첫 사용자 안내 팝업 */} - +
); }; diff --git a/src/utils/eventEmitter.ts b/src/utils/eventEmitter.ts deleted file mode 100644 index 26c65aa..0000000 --- a/src/utils/eventEmitter.ts +++ /dev/null @@ -1,142 +0,0 @@ - -/** - * 이벤트 버스 최적화 - 통합된 이벤트 관리 시스템 - */ - -// 전역 이벤트 이름 정의 -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 = {}; -let eventQueueTimer: ReturnType | 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('[이벤트] 페이지 가시성 이벤트 설정 완료'); -} diff --git a/src/utils/performance/debounceThrottle.ts b/src/utils/performance/debounceThrottle.ts deleted file mode 100644 index ba91773..0000000 --- a/src/utils/performance/debounceThrottle.ts +++ /dev/null @@ -1,118 +0,0 @@ - -/** - * 성능 최적화를 위한 유틸리티 함수 - */ - -// 디바운스: 여러 호출 중 마지막 호출만 실행 -export function debounce any>( - func: T, - wait: number -): (...args: Parameters) => void { - let timeout: ReturnType | null = null; - - return function (...args: Parameters) { - const later = () => { - timeout = null; - func(...args); - }; - - if (timeout) clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -} - -// 스로틀: 일정 시간 내에 한 번만 실행 -export function throttle any>( - func: T, - limit: number -): (...args: Parameters) => void { - let inThrottle = false; - - return function (...args: Parameters) { - if (!inThrottle) { - func(...args); - inThrottle = true; - setTimeout(() => { - inThrottle = false; - }, limit); - } - }; -} - -// 실행 중인 작업 추적 -const pendingOperations: Record = {}; - -// 중복 실행 방지 (한 번에 동일 작업 한 번만 실행) -export function preventDuplicateOperation Promise>( - operationKey: string, - func: T -): (...args: Parameters) => Promise | void> { - return async function (...args: Parameters): Promise | 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 = {}; - -// 캐시 설정 (TTL: 캐시 유효 시간(ms)) -export function withCache Promise>( - cacheKey: string, - func: T, - ttl: number = 60000 // 기본값 1분 -): (...args: Parameters) => Promise> { - return async function (...args: Parameters): Promise> { - 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(updates: () => T): T { - // React의 unstable_batchedUpdates 사용 (가능한 경우) - if (typeof window !== 'undefined' && 'ReactDOM' in window) { - // @ts-ignore: ReactDOM이 전역에 존재할 수 있음 - return window.ReactDOM.unstable_batchedUpdates(updates); - } - - // 폴백: 기본 실행 - return updates(); -} diff --git a/src/utils/sync/syncOptimizer.ts b/src/utils/sync/syncOptimizer.ts deleted file mode 100644 index 0605b6e..0000000 --- a/src/utils/sync/syncOptimizer.ts +++ /dev/null @@ -1,99 +0,0 @@ - -/** - * 동기화 최적화 유틸리티 - */ -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' }; - } -};