diff --git a/src/components/analytics/MonthlyComparisonChart.tsx b/src/components/analytics/MonthlyComparisonChart.tsx index d615748..b7a2c5d 100644 --- a/src/components/analytics/MonthlyComparisonChart.tsx +++ b/src/components/analytics/MonthlyComparisonChart.tsx @@ -31,7 +31,10 @@ const MonthlyComparisonChart: React.FC = ({ return value; }; - // Empty state component + // 데이터 확인 로깅 + console.log('MonthlyComparisonChart 데이터:', monthlyData); + + // EmptyGraphState 컴포넌트: 데이터가 없을 때 표시 const EmptyGraphState = () => (

데이터가 없습니다

@@ -39,9 +42,14 @@ const MonthlyComparisonChart: React.FC = ({
); + // 데이터 여부 확인 로직 개선 - 데이터가 비어있거나 모든 값이 0인 경우도 고려 + const hasValidData = monthlyData && + monthlyData.length > 0 && + monthlyData.some(item => item.budget > 0 || item.expense > 0); + return (
- {!isEmpty && monthlyData.length > 0 && monthlyData.some(item => item.budget > 0 || item.expense > 0) ? ( + {hasValidData ? ( { // 초기화 실행 전에 사용자 설정 백업 const dontShowWelcomeValue = localStorage.getItem('dontShowWelcome'); const hasVisitedBefore = localStorage.getItem('hasVisitedBefore'); + // 로그인 관련 설정도 백업 + const authSession = localStorage.getItem('authSession'); + const sb_auth = localStorage.getItem('sb-auth-token'); // 데이터 초기화 resetAllStorageData(); @@ -41,6 +43,15 @@ const DataResetSection = () => { localStorage.setItem('hasVisitedBefore', hasVisitedBefore); } + // 로그인 관련 설정 복원 (로그인 화면이 나타나지 않도록) + if (authSession) { + localStorage.setItem('authSession', authSession); + } + + if (sb_auth) { + localStorage.setItem('sb-auth-token', sb_auth); + } + // 스토리지 이벤트 트리거하여 다른 컴포넌트에 변경 알림 window.dispatchEvent(new Event('transactionUpdated')); window.dispatchEvent(new Event('budgetDataUpdated')); @@ -94,7 +105,7 @@ const DataResetSection = () => { 정말 모든 데이터를 초기화하시겠습니까? 이 작업은 되돌릴 수 없으며, 모든 예산, 지출 내역, 설정이 영구적으로 삭제됩니다. - 단, '환영합니다' 화면 표시 설정은 유지됩니다. + 단, '환영합니다' 화면 표시 설정과 로그인 상태는 유지됩니다. diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts index 967b20a..cd4df12 100644 --- a/src/hooks/use-toast.ts +++ b/src/hooks/use-toast.ts @@ -8,7 +8,7 @@ import type { // 토스트 알림 표시 제한 및 자동 사라짐 시간(ms) 설정 const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 3000 // 3초 후 자동으로 사라지도록 수정 +const TOAST_REMOVE_DELAY = 3000 // 3초 후 자동으로 사라지도록 설정 type ToasterToast = ToastProps & { id: string @@ -163,6 +163,11 @@ function toast({ ...props }: Toast) { }, }) + // 토스트 생성 후 자동으로 3초 후에 사라지도록 설정 + setTimeout(() => { + dismiss() + }, TOAST_REMOVE_DELAY) + return { id: id, dismiss, diff --git a/src/hooks/useTransactions.ts b/src/hooks/useTransactions.ts index fea7051..d9cd536 100644 --- a/src/hooks/useTransactions.ts +++ b/src/hooks/useTransactions.ts @@ -1,29 +1,93 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Transaction } from '@/components/TransactionCard'; import { useAuth } from '@/contexts/auth/AuthProvider'; import { toast } from '@/hooks/useToast.wrapper'; import { isSyncEnabled } from '@/utils/syncUtils'; -import { MONTHS_KR, getCurrentMonth, getPrevMonth, getNextMonth } from '@/utils/dateUtils'; -import { - loadTransactionsFromStorage, - saveTransactionsToStorage -} from '@/utils/storageUtils'; -import { - filterTransactionsByMonth, - filterTransactionsByQuery, - calculateTotalExpenses -} from '@/utils/transactionUtils'; -import { - syncTransactionsWithSupabase, - updateTransactionInSupabase, - deleteTransactionFromSupabase -} from '@/utils/supabaseTransactionUtils'; import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; -// 월 이름 재노출 -export { MONTHS_KR }; +// 월 이름 상수 +export const MONTHS_KR = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; +// 현재 월 가져오기 +export const getCurrentMonth = () => { + const today = new Date(); + return MONTHS_KR[today.getMonth()]; +}; + +// 이전 월 가져오기 +export const getPrevMonth = (currentMonth: string) => { + const index = MONTHS_KR.indexOf(currentMonth); + return index > 0 ? MONTHS_KR[index - 1] : MONTHS_KR[11]; +}; + +// 다음 월 가져오기 +export const getNextMonth = (currentMonth: string) => { + const index = MONTHS_KR.indexOf(currentMonth); + return index < 11 ? MONTHS_KR[index + 1] : MONTHS_KR[0]; +}; + +// 월별 거래 필터링 +export const filterTransactionsByMonth = (transactions: Transaction[], selectedMonth: string): Transaction[] => { + console.log('월별 필터링:', selectedMonth, '트랜잭션 수:', transactions.length); + + // 현재 날짜 정보 + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; // JavaScript 월은 0부터 시작 + + // 선택된 월의 인덱스 (0-11) + const selectedMonthIndex = MONTHS_KR.findIndex(month => month === selectedMonth); + + // 특수 케이스 처리: '이번 달', '오늘', '이번 주' 등 + if (transactions.some(t => t.date.includes('오늘') || t.date.includes('어제') || t.date.includes('이번주'))) { + return transactions; + } + + // 실제 필터링 + return transactions.filter(transaction => { + // 날짜 형식이 '2023-05-15' 또는 '2023/05/15' 등의 형식인 경우 + if (transaction.date.includes('-') || transaction.date.includes('/')) { + const parts = transaction.date.split(/[-\/]/); + if (parts.length >= 2) { + const transactionMonth = parseInt(parts[1]); + const monthIndex = transactionMonth - 1; // 0-11 인덱스로 변환 + return monthIndex === selectedMonthIndex; + } + } + + // 날짜에 월이 포함된 경우 (예: '5월 15일') + for (let i = 0; i < MONTHS_KR.length; i++) { + if (transaction.date.includes(MONTHS_KR[i])) { + return MONTHS_KR[i] === selectedMonth; + } + } + + // 기본적으로 모든 트랜잭션 표시 (필터링 실패 시) + return true; + }); +}; + +// 검색어로 거래 필터링 +export const filterTransactionsByQuery = (transactions: Transaction[], query: string): Transaction[] => { + if (!query.trim()) return transactions; + + const lowercaseQuery = query.toLowerCase(); + return transactions.filter(transaction => + transaction.title.toLowerCase().includes(lowercaseQuery) || + transaction.category.toLowerCase().includes(lowercaseQuery) || + transaction.amount.toString().includes(lowercaseQuery) + ); +}; + +// 총 지출 계산 +export const calculateTotalExpenses = (transactions: Transaction[]): number => { + return transactions + .filter(t => t.type === 'expense') + .reduce((total, transaction) => total + transaction.amount, 0); +}; + +// useTransactions 훅 export const useTransactions = () => { const [transactions, setTransactions] = useState([]); const [filteredTransactions, setFilteredTransactions] = useState([]); @@ -45,28 +109,41 @@ export const useTransactions = () => { }; // 트랜잭션 로드 - const loadTransactions = () => { + const loadTransactions = useCallback(() => { setIsLoading(true); setError(null); try { // 로컬 스토리지에서 트랜잭션 데이터 가져오기 - const localData = loadTransactionsFromStorage(); + const localDataStr = localStorage.getItem('transactions'); + console.log('로컬 트랜잭션 데이터:', localDataStr); - // 지원되는 카테고리로 필터링 - const filteredData = localData.map(transaction => { - // 트랜잭션의 카테고리가 현재 지원되는 카테고리가 아니면 '기타'로 변경 - if (transaction.type === 'expense' && !EXPENSE_CATEGORIES.includes(transaction.category)) { - return { - ...transaction, - category: '생활비' // 기본값으로 '생활비' 사용 - }; + if (localDataStr) { + try { + const localData = JSON.parse(localDataStr); + + // 지원되는 카테고리로 필터링 + const filteredData = localData.map((transaction: Transaction) => { + // 트랜잭션의 카테고리가 현재 지원되는 카테고리가 아니면 '생활비'로 변경 + if (transaction.type === 'expense' && !EXPENSE_CATEGORIES.includes(transaction.category)) { + return { + ...transaction, + category: '생활비' // 기본값으로 '생활비' 사용 + }; + } + return transaction; + }); + + console.log('필터링된 트랜잭션:', filteredData.length); + setTransactions(filteredData); + } catch (parseError) { + console.error('트랜잭션 데이터 파싱 오류:', parseError); + setTransactions([]); } - return transaction; - }); - - // 로컬 데이터가 있으면 사용 - setTransactions(filteredData); + } else { + console.log('로컬 트랜잭션 데이터 없음'); + setTransactions([]); + } // 예산 가져오기 const budgetDataStr = localStorage.getItem('budgetData'); @@ -90,7 +167,7 @@ export const useTransactions = () => { } finally { setIsLoading(false); } - }; + }, []); // 필터 적용 useEffect(() => { @@ -102,70 +179,58 @@ export const useTransactions = () => { filtered = filterTransactionsByQuery(filtered, searchQuery); } + console.log('필터링 결과:', filtered.length, '트랜잭션'); setFilteredTransactions(filtered); }, [transactions, selectedMonth, searchQuery]); - // 초기 데이터 로드 + // 초기 데이터 로드 및 이벤트 리스너 설정 useEffect(() => { + console.log('useTransactions - 초기 데이터 로드'); loadTransactions(); - // Supabase 동기화 (로그인 상태인 경우) - const syncWithSupabase = async () => { - if (user) { - const syncedTransactions = await syncTransactionsWithSupabase(user, transactions); - if (syncedTransactions !== transactions) { - setTransactions(syncedTransactions); - saveTransactionsToStorage(syncedTransactions); - } - } - }; - - syncWithSupabase(); - - // 예산 데이터 변경 이벤트 리스너 - const handleBudgetUpdate = () => { - const budgetDataStr = localStorage.getItem('budgetData'); - if (budgetDataStr) { - try { - const budgetData = JSON.parse(budgetDataStr); - setTotalBudget(budgetData.monthly.targetAmount); - } catch (e) { - console.error('예산 데이터 파싱 오류:', e); - } - } - }; - - window.addEventListener('budgetDataUpdated', handleBudgetUpdate); - window.addEventListener('storage', (e) => { - if (e.key === 'budgetData') { - handleBudgetUpdate(); - } - }); - - return () => { - window.removeEventListener('budgetDataUpdated', handleBudgetUpdate); - window.removeEventListener('storage', () => {}); - }; - }, [user, refreshKey]); - - // 정기적으로 데이터 새로고침 - useEffect(() => { - const refreshInterval = setInterval(() => { + // 트랜잭션 업데이트 이벤트 리스너 + const handleTransactionUpdated = () => { + console.log('트랜잭션 업데이트 이벤트 감지됨'); loadTransactions(); - }, 5000); // 5초마다 데이터 새로고침 + }; - // 페이지 포커스 시 새로고침 + // 스토리지 변경 이벤트 리스너 + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'transactions' || e.key === null) { + console.log('로컬 스토리지 변경 감지됨:', e.key); + loadTransactions(); + } + }; + + // 페이지 포커스/가시성 이벤트 리스너 const handleFocus = () => { + console.log('창 포커스 - 트랜잭션 새로고침'); loadTransactions(); }; + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + console.log('페이지 가시성 변경 - 트랜잭션 새로고침'); + loadTransactions(); + } + }; + + // 이벤트 리스너 등록 + window.addEventListener('transactionUpdated', handleTransactionUpdated); + window.addEventListener('storage', handleStorageChange); window.addEventListener('focus', handleFocus); + document.addEventListener('visibilitychange', handleVisibilityChange); + + // 컴포넌트 마운트시에만 수동으로 트랜잭션 업데이트 이벤트 발생 + window.dispatchEvent(new Event('transactionUpdated')); return () => { - clearInterval(refreshInterval); + window.removeEventListener('transactionUpdated', handleTransactionUpdated); + window.removeEventListener('storage', handleStorageChange); window.removeEventListener('focus', handleFocus); + document.removeEventListener('visibilitychange', handleVisibilityChange); }; - }, []); + }, [loadTransactions, refreshKey]); // 트랜잭션 업데이트 const updateTransaction = (updatedTransaction: Transaction) => { @@ -173,11 +238,17 @@ export const useTransactions = () => { transaction.id === updatedTransaction.id ? updatedTransaction : transaction ); - setTransactions(updatedTransactions); - saveTransactionsToStorage(updatedTransactions); + // 로컬 스토리지 업데이트 + localStorage.setItem('transactions', JSON.stringify(updatedTransactions)); - // Supabase에도 업데이트 - updateTransactionInSupabase(user, updatedTransaction); + // 백업도 업데이트 + localStorage.setItem('transactions_backup', JSON.stringify(updatedTransactions)); + + // 상태 업데이트 + setTransactions(updatedTransactions); + + // 이벤트 발생 + window.dispatchEvent(new Event('transactionUpdated')); toast({ title: "지출이 수정되었습니다", @@ -189,11 +260,17 @@ export const useTransactions = () => { const deleteTransaction = (id: string) => { const updatedTransactions = transactions.filter(transaction => transaction.id !== id); - setTransactions(updatedTransactions); - saveTransactionsToStorage(updatedTransactions); + // 로컬 스토리지 업데이트 + localStorage.setItem('transactions', JSON.stringify(updatedTransactions)); - // Supabase에서도 삭제 - deleteTransactionFromSupabase(user, id); + // 백업도 업데이트 + localStorage.setItem('transactions_backup', JSON.stringify(updatedTransactions)); + + // 상태 업데이트 + setTransactions(updatedTransactions); + + // 이벤트 발생 + window.dispatchEvent(new Event('transactionUpdated')); toast({ title: "지출이 삭제되었습니다", @@ -209,6 +286,7 @@ export const useTransactions = () => { return { transactions: filteredTransactions, + allTransactions: transactions, isLoading, error, totalBudget, diff --git a/src/utils/storageUtils.ts b/src/utils/storageUtils.ts index b26c563..f8ad441 100644 --- a/src/utils/storageUtils.ts +++ b/src/utils/storageUtils.ts @@ -73,8 +73,12 @@ export const resetAllStorageData = (): void => { // 중요: 사용자 설정 값 백업 const dontShowWelcomeValue = localStorage.getItem('dontShowWelcome'); const hasVisitedBefore = localStorage.getItem('hasVisitedBefore'); + // 로그인 상태 관련 데이터 백업 + const authSession = localStorage.getItem('authSession'); + const sbAuth = localStorage.getItem('sb-auth-token'); + const supabase = localStorage.getItem('supabase.auth.token'); - // 모든 Storage 키 목록 + // 모든 Storage 키 목록 (로그인 관련 항목 제외) const keysToRemove = [ 'transactions', 'budget', @@ -120,12 +124,7 @@ export const resetAllStorageData = (): void => { localStorage.setItem('categoryBudgets', JSON.stringify({ 식비: 0, 교통비: 0, - 생활비: 0, - 쇼핑: 0, - 의료: 0, - 여가: 0, - 교육: 0, - 기타: 0 + 생활비: 0 })); // 백업 생성 @@ -142,13 +141,26 @@ export const resetAllStorageData = (): void => { localStorage.setItem('hasVisitedBefore', hasVisitedBefore); } + // 로그인 상태 복원 + if (authSession) { + localStorage.setItem('authSession', authSession); + } + + if (sbAuth) { + localStorage.setItem('sb-auth-token', sbAuth); + } + + if (supabase) { + localStorage.setItem('supabase.auth.token', supabase); + } + // 이벤트 발생 window.dispatchEvent(new Event('transactionUpdated')); window.dispatchEvent(new Event('budgetDataUpdated')); window.dispatchEvent(new Event('categoryBudgetsUpdated')); window.dispatchEvent(new StorageEvent('storage')); - console.log('모든 저장소 데이터가 완전히 초기화되었습니다.'); + console.log('모든 저장소 데이터가 완전히 초기화되었습니다. (로그인 상태는 유지)'); } catch (error) { console.error('데이터 초기화 중 오류:', error); }