diff --git a/src/components/home/IndexContent.tsx b/src/components/home/IndexContent.tsx new file mode 100644 index 0000000..282fd08 --- /dev/null +++ b/src/components/home/IndexContent.tsx @@ -0,0 +1,58 @@ + +import React from 'react'; +import Header from '@/components/Header'; +import HomeContent from '@/components/home/HomeContent'; +import { useBudget } from '@/contexts/budget/BudgetContext'; +import { BudgetData } from '@/contexts/budget/types'; + +// 기본 예산 데이터 (빈 객체 대신 사용할 더미 데이터) +const defaultBudgetData: BudgetData = { + daily: { + targetAmount: 0, + spentAmount: 0, + remainingAmount: 0 + }, + weekly: { + targetAmount: 0, + spentAmount: 0, + remainingAmount: 0 + }, + monthly: { + targetAmount: 0, + spentAmount: 0, + remainingAmount: 0 + } +}; + +/** + * 인덱스 페이지의 주요 내용을 담당하는 컴포넌트 + */ +const IndexContent: React.FC = () => { + const { + transactions, + budgetData, + selectedTab, + setSelectedTab, + handleBudgetGoalUpdate, + updateTransaction, + getCategorySpending + } = useBudget(); + + return ( +
+
+ + +
+ ); +}; + +export default IndexContent; diff --git a/src/contexts/budget/budgetUtils.ts b/src/contexts/budget/budgetUtils.ts index 07cb20f..3259846 100644 --- a/src/contexts/budget/budgetUtils.ts +++ b/src/contexts/budget/budgetUtils.ts @@ -21,6 +21,31 @@ export const getInitialBudgetData = (): BudgetData => ({ } }); +// 기본 카테고리 예산 값 (수출) +export const DEFAULT_CATEGORY_BUDGETS = {}; + +// 안전한 스토리지 처리 (수출) +export const safeStorage = { + setItem: (key: string, value: any) => { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (err) { + console.error(`스토리지 저장 오류 (${key}):`, err); + return false; + } + }, + getItem: (key: string, defaultValue: any = null) => { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (err) { + console.error(`스토리지 로드 오류 (${key}):`, err); + return defaultValue; + } + } +}; + // 예산 데이터 스토리지에서 로드 export const safelyLoadBudgetData = (): BudgetData => { try { diff --git a/src/hooks/useAppFocusEvents.tsx b/src/hooks/useAppFocusEvents.tsx new file mode 100644 index 0000000..4621509 --- /dev/null +++ b/src/hooks/useAppFocusEvents.tsx @@ -0,0 +1,76 @@ + +import { useEffect } from 'react'; + +/** + * 앱이 포커스를 얻었을 때나 가시성이 변경될 때 데이터를 새로고침하는 커스텀 훅 + */ +export const useAppFocusEvents = () => { + useEffect(() => { + const handleFocus = () => { + try { + console.log('창이 포커스를 얻음 - 데이터 새로고침'); + // 이미 리프레시 중인지 확인하는 플래그 + if (sessionStorage.getItem('isRefreshing') === 'true') { + console.log('이미 리프레시 진행 중, 중복 실행 방지'); + return; + } + + try { + sessionStorage.setItem('isRefreshing', 'true'); + + // 이벤트 발생시켜 데이터 새로고침 + window.dispatchEvent(new Event('storage')); + window.dispatchEvent(new Event('transactionUpdated')); + window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event('categoryBudgetsUpdated')); + + // 리프레시 완료 표시 (300ms 후에 플래그 해제) + setTimeout(() => { + sessionStorage.setItem('isRefreshing', 'false'); + }, 300); + } catch (e) { + console.error('이벤트 발생 오류:', e); + sessionStorage.setItem('isRefreshing', 'false'); + } + } catch (error) { + console.error('포커스 이벤트 처리 중 오류:', error); + } + }; + + // 포커스 이벤트 + window.addEventListener('focus', handleFocus); + + // 가시성 변경 이벤트 (백그라운드에서 전경으로 돌아올 때) + const handleVisibilityChange = () => { + try { + if (document.visibilityState === 'visible') { + console.log('페이지가 다시 보임 - 데이터 새로고침'); + handleFocus(); + } + } catch (error) { + console.error('가시성 이벤트 처리 중 오류:', error); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + // 정기적인 데이터 새로고침 (60초마다로 변경 - 너무 빈번한 리프레시 방지) + const refreshInterval = setInterval(() => { + try { + if (document.visibilityState === 'visible' && + sessionStorage.getItem('isRefreshing') !== 'true') { + console.log('정기 새로고침 - 데이터 업데이트'); + handleFocus(); + } + } catch (error) { + console.error('정기 새로고침 처리 중 오류:', error); + } + }, 60000); // 60초마다 + + return () => { + window.removeEventListener('focus', handleFocus); + document.removeEventListener('visibilitychange', handleVisibilityChange); + clearInterval(refreshInterval); + }; + }, []); +}; diff --git a/src/hooks/useInitialDataLoading.tsx b/src/hooks/useInitialDataLoading.tsx new file mode 100644 index 0000000..ab54d05 --- /dev/null +++ b/src/hooks/useInitialDataLoading.tsx @@ -0,0 +1,57 @@ + +import { useEffect } from 'react'; + +/** + * 앱 첫 실행 시 로컬스토리지 데이터를 로드하는 커스텀 훅 + */ +export const useInitialDataLoading = () => { + useEffect(() => { + try { + console.log('Index 페이지 마운트, 데이터 확인 중...'); + + // 페이지 첫 마운트 시에만 실행되는 로직 + const isFirstMount = sessionStorage.getItem('initialDataLoaded') !== 'true'; + + if (isFirstMount) { + try { + // 백업된 데이터 복구 확인 (메인 데이터가 없는 경우만) + if (!localStorage.getItem('budgetData')) { + const budgetBackup = localStorage.getItem('budgetData_backup'); + if (budgetBackup) { + console.log('예산 데이터 백업에서 복구'); + localStorage.setItem('budgetData', budgetBackup); + } + } + + if (!localStorage.getItem('categoryBudgets')) { + const categoryBackup = localStorage.getItem('categoryBudgets_backup'); + if (categoryBackup) { + console.log('카테고리 예산 백업에서 복구'); + localStorage.setItem('categoryBudgets', categoryBackup); + } + } + + if (!localStorage.getItem('transactions')) { + const transactionBackup = localStorage.getItem('transactions_backup'); + if (transactionBackup) { + console.log('트랜잭션 백업에서 복구'); + localStorage.setItem('transactions', transactionBackup); + } + } + + // 한 번만 이벤트 발생 + window.dispatchEvent(new Event('transactionUpdated')); + window.dispatchEvent(new Event('budgetDataUpdated')); + window.dispatchEvent(new Event('categoryBudgetsUpdated')); + + // 초기 로드 완료 표시 + sessionStorage.setItem('initialDataLoaded', 'true'); + } catch (error) { + console.error('백업 복구 시도 중 오류:', error); + } + } + } catch (error) { + console.error('Index 페이지 초기화 중 오류:', error); + } + }, []); // 컴포넌트 마운트 시 한 번만 실행 +}; diff --git a/src/hooks/useWelcomeNotification.tsx b/src/hooks/useWelcomeNotification.tsx new file mode 100644 index 0000000..f3d0fe6 --- /dev/null +++ b/src/hooks/useWelcomeNotification.tsx @@ -0,0 +1,35 @@ + +import { useEffect } from 'react'; +import { useAuth } from '@/contexts/auth'; +import useNotifications from '@/hooks/useNotifications'; + +/** + * 앱 초기화 후 환영 메시지 알림을 표시하는 커스텀 훅 + */ +export const useWelcomeNotification = (isInitialized: boolean) => { + const { user } = useAuth(); + const { addNotification } = useNotifications(); + + useEffect(() => { + try { + // 환영 메시지가 이미 표시되었는지 확인하는 키 + const welcomeNotificationSent = sessionStorage.getItem('welcomeNotificationSent'); + + if (isInitialized && user && !welcomeNotificationSent) { + // 사용자 로그인 시 알림 예시 (한 번만 실행) + const timeoutId = setTimeout(() => { + addNotification( + '환영합니다!', + '젤리의 적자탈출에 오신 것을 환영합니다. 예산을 설정하고 지출을 기록해보세요.' + ); + // 세션 스토리지에 환영 메시지 표시 여부 저장 + sessionStorage.setItem('welcomeNotificationSent', 'true'); + }, 2000); + + return () => clearTimeout(timeoutId); + } + } catch (error) { + console.error('환영 메시지 알림 표시 중 오류:', error); + } + }, [isInitialized, user, addNotification]); +}; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 9d36034..eabb2fe 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -2,55 +2,28 @@ import React, { useEffect } from 'react'; import NavBar from '@/components/NavBar'; import AddTransactionButton from '@/components/AddTransactionButton'; -import Header from '@/components/Header'; import WelcomeDialog from '@/components/onboarding/WelcomeDialog'; -import HomeContent from '@/components/home/HomeContent'; +import IndexContent from '@/components/home/IndexContent'; import { useBudget } from '@/contexts/budget/BudgetContext'; -import { useAuth } from '@/contexts/auth'; import { useWelcomeDialog } from '@/hooks/useWelcomeDialog'; import { useDataInitialization } from '@/hooks/useDataInitialization'; -import { useIsMobile } from '@/hooks/use-mobile'; -import useNotifications from '@/hooks/useNotifications'; import SafeAreaContainer from '@/components/SafeAreaContainer'; -import { BudgetData } from '@/contexts/budget/types'; +import { useInitialDataLoading } from '@/hooks/useInitialDataLoading'; +import { useAppFocusEvents } from '@/hooks/useAppFocusEvents'; +import { useWelcomeNotification } from '@/hooks/useWelcomeNotification'; -// 기본 예산 데이터 (빈 객체 대신 사용할 더미 데이터) -const defaultBudgetData: BudgetData = { - daily: { - targetAmount: 0, - spentAmount: 0, - remainingAmount: 0 - }, - weekly: { - targetAmount: 0, - spentAmount: 0, - remainingAmount: 0 - }, - monthly: { - targetAmount: 0, - spentAmount: 0, - remainingAmount: 0 - } -}; - -// 메인 컴포넌트 +/** + * 애플리케이션의 메인 인덱스 페이지 컴포넌트 + */ const Index = () => { - const { - transactions, - budgetData, - selectedTab, - setSelectedTab, - handleBudgetGoalUpdate, - updateTransaction, - getCategorySpending, - resetBudgetData - } = useBudget(); - - const { user } = useAuth(); + const { resetBudgetData } = useBudget(); const { showWelcome, checkWelcomeDialogState, handleCloseWelcome } = useWelcomeDialog(); const { isInitialized } = useDataInitialization(resetBudgetData); - const isMobile = useIsMobile(); - const { addNotification } = useNotifications(); + + // 커스텀 훅 사용으로 코드 분리 + useInitialDataLoading(); + useAppFocusEvents(); + useWelcomeNotification(isInitialized); // 초기화 후 환영 메시지 표시 상태 확인 useEffect(() => { @@ -60,168 +33,9 @@ const Index = () => { } }, [isInitialized, checkWelcomeDialogState]); - // 앱 시작시 예시 알림 추가 (실제 앱에서는 필요한 이벤트에 따라 알림 추가) - useEffect(() => { - try { - // 환영 메시지가 이미 표시되었는지 확인하는 키 - const welcomeNotificationSent = sessionStorage.getItem('welcomeNotificationSent'); - - if (isInitialized && user && !welcomeNotificationSent) { - // 사용자 로그인 시 알림 예시 (한 번만 실행) - const timeoutId = setTimeout(() => { - addNotification( - '환영합니다!', - '젤리의 적자탈출에 오신 것을 환영합니다. 예산을 설정하고 지출을 기록해보세요.' - ); - // 세션 스토리지에 환영 메시지 표시 여부 저장 - sessionStorage.setItem('welcomeNotificationSent', 'true'); - }, 2000); - - return () => clearTimeout(timeoutId); - } - } catch (error) { - console.error('환영 메시지 알림 표시 중 오류:', error); - } - }, [isInitialized, user, addNotification]); - - // 페이지가 처음 로드될 때 데이터 로딩 확인 - 에러 방지를 위해 try/catch 추가 - useEffect(() => { - try { - console.log('Index 페이지 마운트, 현재 데이터 상태:'); - console.log('트랜잭션:', transactions?.length || 0); - console.log('예산 데이터:', budgetData || defaultBudgetData); - - // 페이지 첫 마운트 시에만 실행되는 로직으로 수정 - const isFirstMount = sessionStorage.getItem('initialDataLoaded') !== 'true'; - - if (isFirstMount) { - try { - // 백업된 데이터 복구 확인 (메인 데이터가 없는 경우만) - if (!localStorage.getItem('budgetData')) { - const budgetBackup = localStorage.getItem('budgetData_backup'); - if (budgetBackup) { - console.log('예산 데이터 백업에서 복구'); - localStorage.setItem('budgetData', budgetBackup); - } - } - - if (!localStorage.getItem('categoryBudgets')) { - const categoryBackup = localStorage.getItem('categoryBudgets_backup'); - if (categoryBackup) { - console.log('카테고리 예산 백업에서 복구'); - localStorage.setItem('categoryBudgets', categoryBackup); - } - } - - if (!localStorage.getItem('transactions')) { - const transactionBackup = localStorage.getItem('transactions_backup'); - if (transactionBackup) { - console.log('트랜잭션 백업에서 복구'); - localStorage.setItem('transactions', transactionBackup); - } - } - - // 한 번만 이벤트 발생 - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - - // 초기 로드 완료 표시 - sessionStorage.setItem('initialDataLoaded', 'true'); - } catch (error) { - console.error('백업 복구 시도 중 오류:', error); - } - } - } catch (error) { - console.error('Index 페이지 초기화 중 오류:', error); - } - }, []); // 의존성 배열 비움 - 컴포넌트 마운트 시 한 번만 실행 - - // 앱이 포커스를 얻었을 때 데이터를 새로고침 - useEffect(() => { - const handleFocus = () => { - try { - console.log('창이 포커스를 얻음 - 데이터 새로고침'); - // 이미 리프레시 중인지 확인하는 플래그 - if (sessionStorage.getItem('isRefreshing') === 'true') { - console.log('이미 리프레시 진행 중, 중복 실행 방지'); - return; - } - - try { - sessionStorage.setItem('isRefreshing', 'true'); - - // 이벤트 발생시켜 데이터 새로고침 - window.dispatchEvent(new Event('storage')); - window.dispatchEvent(new Event('transactionUpdated')); - window.dispatchEvent(new Event('budgetDataUpdated')); - window.dispatchEvent(new Event('categoryBudgetsUpdated')); - - // 리프레시 완료 표시 (300ms 후에 플래그 해제) - setTimeout(() => { - sessionStorage.setItem('isRefreshing', 'false'); - }, 300); - } catch (e) { - console.error('이벤트 발생 오류:', e); - sessionStorage.setItem('isRefreshing', 'false'); - } - } catch (error) { - console.error('포커스 이벤트 처리 중 오류:', error); - } - }; - - // 포커스 이벤트 - window.addEventListener('focus', handleFocus); - - // 가시성 변경 이벤트 (백그라운드에서 전경으로 돌아올 때) - const handleVisibilityChange = () => { - try { - if (document.visibilityState === 'visible') { - console.log('페이지가 다시 보임 - 데이터 새로고침'); - handleFocus(); - } - } catch (error) { - console.error('가시성 이벤트 처리 중 오류:', error); - } - }; - - document.addEventListener('visibilitychange', handleVisibilityChange); - - // 정기적인 데이터 새로고침 (60초마다로 변경 - 너무 빈번한 리프레시 방지) - const refreshInterval = setInterval(() => { - try { - if (document.visibilityState === 'visible' && - sessionStorage.getItem('isRefreshing') !== 'true') { - console.log('정기 새로고침 - 데이터 업데이트'); - handleFocus(); - } - } catch (error) { - console.error('정기 새로고침 처리 중 오류:', error); - } - }, 60000); // 10초에서 60초로 변경 - - return () => { - window.removeEventListener('focus', handleFocus); - document.removeEventListener('visibilitychange', handleVisibilityChange); - clearInterval(refreshInterval); - }; - }, []); - return ( -
-
- - -
+