Reverted to edit edt-fb357fcb-7bf8-4196-adc5-e2dc2fed4b19: "Fix notch issue on iOS
Addresses the notch display issue on iOS devices."
This commit is contained in:
@@ -1,380 +1,23 @@
|
|||||||
|
|
||||||
import React, { createContext, useState, useContext, useEffect, useCallback } from 'react';
|
import React from 'react';
|
||||||
import { Transaction, BudgetData } from './types';
|
import { useBudgetState } from './useBudgetState';
|
||||||
import {
|
import { BudgetContext, BudgetContextType } from './useBudget';
|
||||||
safelyLoadBudgetData,
|
import { BudgetPeriod, Transaction } from './types';
|
||||||
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={contextValue}>
|
<BudgetContext.Provider value={budgetState}>
|
||||||
{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';
|
||||||
|
|||||||
@@ -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<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,9 +11,6 @@ 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 = () => {
|
||||||
@@ -33,12 +30,6 @@ 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(() => {
|
||||||
@@ -68,45 +59,98 @@ 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'));
|
||||||
if (!localStorage.getItem('budgetData') && localStorage.getItem('budgetData_backup')) {
|
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||||
localStorage.setItem('budgetData', localStorage.getItem('budgetData_backup')!);
|
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||||
emitEvent(APP_EVENTS.BUDGET_UPDATED);
|
} catch (e) {
|
||||||
|
console.error('이벤트 발생 오류:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!localStorage.getItem('categoryBudgets') && localStorage.getItem('categoryBudgets_backup')) {
|
// 백업된 데이터 복구 확인 (메인 데이터가 없는 경우만)
|
||||||
localStorage.setItem('categoryBudgets', localStorage.getItem('categoryBudgets_backup')!);
|
try {
|
||||||
emitEvent(APP_EVENTS.CATEGORY_UPDATED);
|
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('transactions') && localStorage.getItem('transactions_backup')) {
|
if (!localStorage.getItem('categoryBudgets')) {
|
||||||
localStorage.setItem('transactions', localStorage.getItem('transactions_backup')!);
|
const categoryBackup = localStorage.getItem('categoryBudgets_backup');
|
||||||
emitEvent(APP_EVENTS.TRANSACTION_UPDATED);
|
if (categoryBackup) {
|
||||||
|
console.log('카테고리 예산 백업에서 복구');
|
||||||
|
localStorage.setItem('categoryBudgets', categoryBackup);
|
||||||
|
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 로그인 상태면 동기화 수행 (지연 시작으로 초기 로딩 성능 개선)
|
if (!localStorage.getItem('transactions')) {
|
||||||
if (user) {
|
const transactionBackup = localStorage.getItem('transactions_backup');
|
||||||
setTimeout(() => {
|
if (transactionBackup) {
|
||||||
syncNow();
|
console.log('트랜잭션 백업에서 복구');
|
||||||
}, 1500);
|
localStorage.setItem('transactions', transactionBackup);
|
||||||
|
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 (
|
||||||
<SafeAreaContainer className="min-h-screen bg-neuro-background">
|
<div className="min-h-screen bg-neuro-background pb-24">
|
||||||
<div className="max-w-md mx-auto px-6 pb-24">
|
<div className="max-w-md mx-auto px-6">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<HomeContent
|
<HomeContent
|
||||||
@@ -124,7 +168,7 @@ const Index = () => {
|
|||||||
|
|
||||||
{/* 첫 사용자 안내 팝업 */}
|
{/* 첫 사용자 안내 팝업 */}
|
||||||
<WelcomeDialog open={showWelcome} onClose={handleCloseWelcome} />
|
<WelcomeDialog open={showWelcome} onClose={handleCloseWelcome} />
|
||||||
</SafeAreaContainer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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<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('[이벤트] 페이지 가시성 이벤트 설정 완료');
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* 성능 최적화를 위한 유틸리티 함수
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 디바운스: 여러 호출 중 마지막 호출만 실행
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
@@ -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' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user