From a326981e5d607db14ba39e2132af2f7e0124ad9c Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 5 Apr 2025 05:31:21 +0000 Subject: [PATCH] Refactor: Split Index.tsx into smaller components and hooks Split the large Index.tsx file into smaller, more manageable components and custom hooks to improve code readability and maintainability. Ensure all functionality remains the same after refactoring. --- src/components/home/IndexContent.tsx | 58 ++++++++ src/contexts/budget/budgetUtils.ts | 25 ++++ src/hooks/useAppFocusEvents.tsx | 76 ++++++++++ src/hooks/useInitialDataLoading.tsx | 57 +++++++ src/hooks/useWelcomeNotification.tsx | 35 +++++ src/pages/Index.tsx | 214 ++------------------------- 6 files changed, 265 insertions(+), 200 deletions(-) create mode 100644 src/components/home/IndexContent.tsx create mode 100644 src/hooks/useAppFocusEvents.tsx create mode 100644 src/hooks/useInitialDataLoading.tsx create mode 100644 src/hooks/useWelcomeNotification.tsx 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 ( -
-
- - -
+