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:
gpt-engineer-app[bot]
2025-03-23 10:23:55 +00:00
parent d216daf2f4
commit 606ac2f96a
6 changed files with 93 additions and 870 deletions

View File

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

View File

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

View File

@@ -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);
}
// 백업된 데이터 복구 확인 (메인 데이터가 없는 경우만)
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')) { if (!localStorage.getItem('categoryBudgets')) {
localStorage.setItem('categoryBudgets', localStorage.getItem('categoryBudgets_backup')!); const categoryBackup = localStorage.getItem('categoryBudgets_backup');
emitEvent(APP_EVENTS.CATEGORY_UPDATED); if (categoryBackup) {
console.log('카테고리 예산 백업에서 복구');
localStorage.setItem('categoryBudgets', categoryBackup);
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
}
} }
if (!localStorage.getItem('transactions') && localStorage.getItem('transactions_backup')) { if (!localStorage.getItem('transactions')) {
localStorage.setItem('transactions', localStorage.getItem('transactions_backup')!); const transactionBackup = localStorage.getItem('transactions_backup');
emitEvent(APP_EVENTS.TRANSACTION_UPDATED); if (transactionBackup) {
} console.log('트랜잭션 백업에서 복구');
localStorage.setItem('transactions', transactionBackup);
// 로그인 상태면 동기화 수행 (지연 시작으로 초기 로딩 성능 개선) window.dispatchEvent(new Event('transactionUpdated'));
if (user) { }
setTimeout(() => {
syncNow();
}, 1500);
} }
} 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>
); );
}; };

View File

@@ -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('[이벤트] 페이지 가시성 이벤트 설정 완료');
}

View File

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

View File

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