diff --git a/src/hooks/useTransactions.ts b/src/hooks/useTransactions.ts index 2be514d..2e0e43d 100644 --- a/src/hooks/useTransactions.ts +++ b/src/hooks/useTransactions.ts @@ -1,22 +1,29 @@ import { useState, useEffect } from 'react'; import { Transaction } from '@/components/TransactionCard'; -import { supabase } from '@/lib/supabase'; 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, + createSampleTransactions, + saveTransactionsToStorage, + loadBudgetFromStorage +} from '@/utils/storageUtils'; +import { + filterTransactionsByMonth, + filterTransactionsByQuery, + calculateTotalExpenses +} from '@/utils/transactionUtils'; +import { + syncTransactionsWithSupabase, + updateTransactionInSupabase, + deleteTransactionFromSupabase +} from '@/utils/supabaseTransactionUtils'; -// 월 이름 정의 -export const MONTHS_KR = [ - '1월', '2월', '3월', '4월', '5월', '6월', - '7월', '8월', '9월', '10월', '11월', '12월' -]; - -// 현재 월 가져오기 -const getCurrentMonth = () => { - const now = new Date(); - return MONTHS_KR[now.getMonth()]; -}; +// 월 이름 재노출 +export { MONTHS_KR }; export const useTransactions = () => { const [transactions, setTransactions] = useState([]); @@ -30,21 +37,11 @@ export const useTransactions = () => { // 월 변경 처리 const handlePrevMonth = () => { - const currentIndex = MONTHS_KR.indexOf(selectedMonth); - if (currentIndex > 0) { - setSelectedMonth(MONTHS_KR[currentIndex - 1]); - } else { - setSelectedMonth(MONTHS_KR[MONTHS_KR.length - 1]); - } + setSelectedMonth(getPrevMonth(selectedMonth)); }; const handleNextMonth = () => { - const currentIndex = MONTHS_KR.indexOf(selectedMonth); - if (currentIndex < MONTHS_KR.length - 1) { - setSelectedMonth(MONTHS_KR[currentIndex + 1]); - } else { - setSelectedMonth(MONTHS_KR[0]); - } + setSelectedMonth(getNextMonth(selectedMonth)); }; // 트랜잭션 로드 @@ -54,60 +51,20 @@ export const useTransactions = () => { try { // 로컬 스토리지에서 트랜잭션 데이터 가져오기 - const localData = localStorage.getItem('transactions'); + const localData = loadTransactionsFromStorage(); - if (localData) { - const parsedData = JSON.parse(localData) as Transaction[]; - setTransactions(parsedData); + if (localData.length > 0) { + setTransactions(localData); } else { - // 샘플 데이터 설정 (실제 앱에서는 빈 배열로 시작할 수 있음) - const sampleData: Transaction[] = [{ - id: '1', - title: '식료품 구매', - amount: 25000, - date: `${selectedMonth} 25일, 12:30 PM`, - category: '식비', - type: 'expense' - }, { - id: '2', - title: '주유소', - amount: 50000, - date: `${selectedMonth} 24일, 3:45 PM`, - category: '교통비', - type: 'expense' - }, { - id: '4', - title: '생필품 구매', - amount: 35000, - date: `${selectedMonth} 18일, 6:00 AM`, - category: '생활비', - type: 'expense' - }, { - id: '5', - title: '월세', - amount: 650000, - date: `${selectedMonth} 15일, 10:00 AM`, - category: '생활비', - type: 'expense' - }, { - id: '6', - title: '식당', - amount: 15500, - date: `${selectedMonth} 12일, 2:15 PM`, - category: '식비', - type: 'expense' - }]; - + // 샘플 데이터 설정 + const sampleData = createSampleTransactions(selectedMonth); setTransactions(sampleData); - localStorage.setItem('transactions', JSON.stringify(sampleData)); + saveTransactionsToStorage(sampleData); } // 예산 가져오기 - const budgetData = localStorage.getItem('budget'); - if (budgetData) { - const parsedBudget = JSON.parse(budgetData); - setTotalBudget(parsedBudget.total || 1000000); - } + const budget = loadBudgetFromStorage(); + setTotalBudget(budget); } catch (err) { console.error('트랜잭션 로드 중 오류:', err); setError('데이터를 불러오는 중 문제가 발생했습니다.'); @@ -124,17 +81,11 @@ export const useTransactions = () => { // 필터 적용 useEffect(() => { // 1. 월별 필터링 - let filtered = transactions.filter(transaction => - transaction.date.includes(selectedMonth) && transaction.type === 'expense' - ); + let filtered = filterTransactionsByMonth(transactions, selectedMonth); - // 2. 검색어 필터링 (검색어가 있는 경우) + // 2. 검색어 필터링 if (searchQuery.trim()) { - const query = searchQuery.toLowerCase().trim(); - filtered = filtered.filter(transaction => - transaction.title.toLowerCase().includes(query) || - transaction.category.toLowerCase().includes(query) - ); + filtered = filterTransactionsByQuery(filtered, searchQuery); } setFilteredTransactions(filtered); @@ -146,44 +97,11 @@ export const useTransactions = () => { // Supabase 동기화 (로그인 상태인 경우) const syncWithSupabase = async () => { - if (user && isSyncEnabled()) { - try { - const { data, error } = await supabase - .from('transactions') - .select('*') - .eq('user_id', user.id); - - if (error) throw error; - - if (data && data.length > 0) { - // Supabase 데이터 로컬 형식으로 변환 - const supabaseTransactions = data.map(t => ({ - id: t.transaction_id || t.id, - title: t.title, - amount: t.amount, - date: t.date, - category: t.category, - type: t.type - })); - - // 로컬 데이터와 병합 (중복 ID 제거) - const mergedTransactions = [...transactions]; - - supabaseTransactions.forEach(newTx => { - const existingIndex = mergedTransactions.findIndex(t => t.id === newTx.id); - if (existingIndex >= 0) { - mergedTransactions[existingIndex] = newTx; - } else { - mergedTransactions.push(newTx); - } - }); - - setTransactions(mergedTransactions); - localStorage.setItem('transactions', JSON.stringify(mergedTransactions)); - } - } catch (err) { - console.error('Supabase 동기화 오류:', err); - // 동기화 실패해도 기존 로컬 데이터는 유지 + if (user) { + const syncedTransactions = await syncTransactionsWithSupabase(user, transactions); + if (syncedTransactions !== transactions) { + setTransactions(syncedTransactions); + saveTransactionsToStorage(syncedTransactions); } } }; @@ -198,26 +116,10 @@ export const useTransactions = () => { ); setTransactions(updatedTransactions); - localStorage.setItem('transactions', JSON.stringify(updatedTransactions)); + saveTransactionsToStorage(updatedTransactions); - // Supabase에도 업데이트 (로그인 및 동기화 활성화된 경우) - if (user && isSyncEnabled()) { - supabase.from('transactions') - .upsert({ - user_id: user.id, - title: updatedTransaction.title, - amount: updatedTransaction.amount, - date: updatedTransaction.date, - category: updatedTransaction.category, - type: updatedTransaction.type, - transaction_id: updatedTransaction.id - }) - .then(({ error }) => { - if (error) { - console.error('Supabase 업데이트 오류:', error); - } - }); - } + // Supabase에도 업데이트 + updateTransactionInSupabase(user, updatedTransaction); toast({ title: "지출이 수정되었습니다", @@ -230,19 +132,10 @@ export const useTransactions = () => { const updatedTransactions = transactions.filter(transaction => transaction.id !== id); setTransactions(updatedTransactions); - localStorage.setItem('transactions', JSON.stringify(updatedTransactions)); + saveTransactionsToStorage(updatedTransactions); - // Supabase에서도 삭제 (로그인 및 동기화 활성화된 경우) - if (user && isSyncEnabled()) { - supabase.from('transactions') - .delete() - .eq('transaction_id', id) - .then(({ error }) => { - if (error) { - console.error('Supabase 삭제 오류:', error); - } - }); - } + // Supabase에서도 삭제 + deleteTransactionFromSupabase(user, id); toast({ title: "지출이 삭제되었습니다", @@ -262,7 +155,7 @@ export const useTransactions = () => { handleNextMonth, updateTransaction, deleteTransaction, - totalExpenses: filteredTransactions.reduce((sum, t) => sum + t.amount, 0), + totalExpenses: calculateTotalExpenses(filteredTransactions), refreshTransactions: loadTransactions }; }; diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts new file mode 100644 index 0000000..423b603 --- /dev/null +++ b/src/utils/dateUtils.ts @@ -0,0 +1,32 @@ + +// 월 이름 정의 +export const MONTHS_KR = [ + '1월', '2월', '3월', '4월', '5월', '6월', + '7월', '8월', '9월', '10월', '11월', '12월' +]; + +// 현재 월 가져오기 +export const getCurrentMonth = () => { + const now = new Date(); + return MONTHS_KR[now.getMonth()]; +}; + +// 이전 달 가져오기 +export const getPrevMonth = (currentMonth: string) => { + const currentIndex = MONTHS_KR.indexOf(currentMonth); + if (currentIndex > 0) { + return MONTHS_KR[currentIndex - 1]; + } else { + return MONTHS_KR[MONTHS_KR.length - 1]; + } +}; + +// 다음 달 가져오기 +export const getNextMonth = (currentMonth: string) => { + const currentIndex = MONTHS_KR.indexOf(currentMonth); + if (currentIndex < MONTHS_KR.length - 1) { + return MONTHS_KR[currentIndex + 1]; + } else { + return MONTHS_KR[0]; + } +}; diff --git a/src/utils/storageUtils.ts b/src/utils/storageUtils.ts new file mode 100644 index 0000000..5458484 --- /dev/null +++ b/src/utils/storageUtils.ts @@ -0,0 +1,66 @@ + +import { Transaction } from '@/components/TransactionCard'; + +// 트랜잭션 데이터 불러오기 +export const loadTransactionsFromStorage = (): Transaction[] => { + const localData = localStorage.getItem('transactions'); + if (localData) { + return JSON.parse(localData) as Transaction[]; + } + return []; +}; + +// 기본 샘플 데이터 생성 +export const createSampleTransactions = (selectedMonth: string): Transaction[] => { + return [{ + id: '1', + title: '식료품 구매', + amount: 25000, + date: `${selectedMonth} 25일, 12:30 PM`, + category: '식비', + type: 'expense' + }, { + id: '2', + title: '주유소', + amount: 50000, + date: `${selectedMonth} 24일, 3:45 PM`, + category: '교통비', + type: 'expense' + }, { + id: '4', + title: '생필품 구매', + amount: 35000, + date: `${selectedMonth} 18일, 6:00 AM`, + category: '생활비', + type: 'expense' + }, { + id: '5', + title: '월세', + amount: 650000, + date: `${selectedMonth} 15일, 10:00 AM`, + category: '생활비', + type: 'expense' + }, { + id: '6', + title: '식당', + amount: 15500, + date: `${selectedMonth} 12일, 2:15 PM`, + category: '식비', + type: 'expense' + }]; +}; + +// 트랜잭션 데이터 저장하기 +export const saveTransactionsToStorage = (transactions: Transaction[]): void => { + localStorage.setItem('transactions', JSON.stringify(transactions)); +}; + +// 예산 불러오기 +export const loadBudgetFromStorage = (): number => { + const budgetData = localStorage.getItem('budget'); + if (budgetData) { + const parsedBudget = JSON.parse(budgetData); + return parsedBudget.total || 1000000; + } + return 1000000; // 기본 예산 +}; diff --git a/src/utils/supabaseTransactionUtils.ts b/src/utils/supabaseTransactionUtils.ts new file mode 100644 index 0000000..f2f359d --- /dev/null +++ b/src/utils/supabaseTransactionUtils.ts @@ -0,0 +1,91 @@ + +import { supabase } from '@/lib/supabase'; +import { Transaction } from '@/components/TransactionCard'; +import { isSyncEnabled } from '@/utils/syncUtils'; +import { toast } from '@/hooks/useToast.wrapper'; + +// Supabase와 거래 데이터 동기화 +export const syncTransactionsWithSupabase = async ( + user: any, + transactions: Transaction[] +): Promise => { + if (!user || !isSyncEnabled()) return transactions; + + try { + const { data, error } = await supabase + .from('transactions') + .select('*') + .eq('user_id', user.id); + + if (error) throw error; + + if (data && data.length > 0) { + // Supabase 데이터 로컬 형식으로 변환 + const supabaseTransactions = data.map(t => ({ + id: t.transaction_id || t.id, + title: t.title, + amount: t.amount, + date: t.date, + category: t.category, + type: t.type + })); + + // 로컬 데이터와 병합 (중복 ID 제거) + const mergedTransactions = [...transactions]; + + supabaseTransactions.forEach(newTx => { + const existingIndex = mergedTransactions.findIndex(t => t.id === newTx.id); + if (existingIndex >= 0) { + mergedTransactions[existingIndex] = newTx; + } else { + mergedTransactions.push(newTx); + } + }); + + return mergedTransactions; + } + } catch (err) { + console.error('Supabase 동기화 오류:', err); + } + + return transactions; +}; + +// Supabase에 트랜잭션 업데이트 +export const updateTransactionInSupabase = async ( + user: any, + transaction: Transaction +): Promise => { + if (!user || !isSyncEnabled()) return; + + try { + await supabase.from('transactions') + .upsert({ + user_id: user.id, + title: transaction.title, + amount: transaction.amount, + date: transaction.date, + category: transaction.category, + type: transaction.type, + transaction_id: transaction.id + }); + } catch (error) { + console.error('Supabase 업데이트 오류:', error); + } +}; + +// Supabase에서 트랜잭션 삭제 +export const deleteTransactionFromSupabase = async ( + user: any, + transactionId: string +): Promise => { + if (!user || !isSyncEnabled()) return; + + try { + await supabase.from('transactions') + .delete() + .eq('transaction_id', transactionId); + } catch (error) { + console.error('Supabase 삭제 오류:', error); + } +}; diff --git a/src/utils/transactionUtils.ts b/src/utils/transactionUtils.ts new file mode 100644 index 0000000..14a018b --- /dev/null +++ b/src/utils/transactionUtils.ts @@ -0,0 +1,31 @@ + +import { Transaction } from '@/components/TransactionCard'; + +// 월별로 트랜잭션 필터링 +export const filterTransactionsByMonth = ( + transactions: Transaction[], + selectedMonth: string +): Transaction[] => { + return transactions.filter(transaction => + transaction.date.includes(selectedMonth) && transaction.type === 'expense' + ); +}; + +// 검색어로 트랜잭션 필터링 +export const filterTransactionsByQuery = ( + transactions: Transaction[], + searchQuery: string +): Transaction[] => { + if (!searchQuery.trim()) return transactions; + + const query = searchQuery.toLowerCase().trim(); + return transactions.filter(transaction => + transaction.title.toLowerCase().includes(query) || + transaction.category.toLowerCase().includes(query) + ); +}; + +// 총 지출 금액 계산 +export const calculateTotalExpenses = (transactions: Transaction[]): number => { + return transactions.reduce((sum, t) => sum + t.amount, 0); +};