Refactor Transactions page
Refactors the Transactions page into smaller, more manageable components to improve code organization and maintainability. The functionality remains the same.
This commit is contained in:
37
src/components/transactions/EmptyTransactions.tsx
Normal file
37
src/components/transactions/EmptyTransactions.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface EmptyTransactionsProps {
|
||||
searchQuery: string;
|
||||
selectedMonth: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
const EmptyTransactions: React.FC<EmptyTransactionsProps> = ({
|
||||
searchQuery,
|
||||
selectedMonth,
|
||||
setSearchQuery,
|
||||
isDisabled
|
||||
}) => {
|
||||
return (
|
||||
<div className="text-center py-10">
|
||||
<p className="text-gray-500 mb-3">
|
||||
{searchQuery.trim()
|
||||
? '검색 결과가 없습니다.'
|
||||
: `${selectedMonth}에 등록된 지출이 없습니다.`}
|
||||
</p>
|
||||
{searchQuery.trim() && (
|
||||
<button
|
||||
className="text-neuro-income"
|
||||
onClick={() => setSearchQuery('')}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
검색 초기화
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyTransactions;
|
||||
37
src/components/transactions/TransactionDateGroup.tsx
Normal file
37
src/components/transactions/TransactionDateGroup.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
import React from 'react';
|
||||
import TransactionCard, { Transaction } from '@/components/TransactionCard';
|
||||
|
||||
interface TransactionDateGroupProps {
|
||||
date: string;
|
||||
transactions: Transaction[];
|
||||
onTransactionDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
const TransactionDateGroup: React.FC<TransactionDateGroupProps> = ({
|
||||
date,
|
||||
transactions,
|
||||
onTransactionDelete
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="h-1 flex-1 neuro-pressed"></div>
|
||||
<h2 className="text-sm font-medium text-gray-500">{date}</h2>
|
||||
<div className="h-1 flex-1 neuro-pressed"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{transactions.map(transaction => (
|
||||
<TransactionCard
|
||||
key={transaction.id}
|
||||
transaction={transaction}
|
||||
onDelete={onTransactionDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionDateGroup;
|
||||
61
src/components/transactions/TransactionsContent.tsx
Normal file
61
src/components/transactions/TransactionsContent.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import TransactionsList from './TransactionsList';
|
||||
import EmptyTransactions from './EmptyTransactions';
|
||||
|
||||
interface TransactionsContentProps {
|
||||
isLoading: boolean;
|
||||
isProcessing: boolean;
|
||||
transactions: Transaction[];
|
||||
groupedTransactions: Record<string, Transaction[]>;
|
||||
searchQuery: string;
|
||||
selectedMonth: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
onTransactionDelete: (id: string) => void;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
const TransactionsContent: React.FC<TransactionsContentProps> = ({
|
||||
isLoading,
|
||||
isProcessing,
|
||||
transactions,
|
||||
groupedTransactions,
|
||||
searchQuery,
|
||||
selectedMonth,
|
||||
setSearchQuery,
|
||||
onTransactionDelete,
|
||||
isDisabled
|
||||
}) => {
|
||||
if (isLoading || isProcessing) {
|
||||
return <LoadingState isProcessing={isProcessing} />;
|
||||
}
|
||||
|
||||
if (!isLoading && !isProcessing && transactions.length === 0) {
|
||||
return (
|
||||
<EmptyTransactions
|
||||
searchQuery={searchQuery}
|
||||
selectedMonth={selectedMonth}
|
||||
setSearchQuery={setSearchQuery}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TransactionsList
|
||||
groupedTransactions={groupedTransactions}
|
||||
onTransactionDelete={onTransactionDelete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingState: React.FC<{ isProcessing: boolean }> = ({ isProcessing }) => (
|
||||
<div className="flex justify-center items-center py-10">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-neuro-income" />
|
||||
<span className="ml-2 text-gray-500">{isProcessing ? '처리 중...' : '로딩 중...'}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TransactionsContent;
|
||||
87
src/components/transactions/TransactionsHeader.tsx
Normal file
87
src/components/transactions/TransactionsHeader.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Calendar, Search, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { formatCurrency } from '@/utils/formatters';
|
||||
|
||||
interface TransactionsHeaderProps {
|
||||
selectedMonth: string;
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
handlePrevMonth: () => void;
|
||||
handleNextMonth: () => void;
|
||||
budgetData: any;
|
||||
totalExpenses: number;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
|
||||
selectedMonth,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
handlePrevMonth,
|
||||
handleNextMonth,
|
||||
budgetData,
|
||||
totalExpenses,
|
||||
isDisabled
|
||||
}) => {
|
||||
return (
|
||||
<header className="py-8">
|
||||
<h1 className="text-2xl font-bold neuro-text mb-5">지출 내역</h1>
|
||||
|
||||
{/* Search */}
|
||||
<div className="neuro-pressed mb-5 flex items-center px-4 py-3 rounded-xl">
|
||||
<Search size={18} className="text-gray-500 mr-2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="지출 검색..."
|
||||
className="bg-transparent flex-1 outline-none text-sm"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Month Selector */}
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<button
|
||||
className="neuro-flat p-2 rounded-full"
|
||||
onClick={handlePrevMonth}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={18} className="text-neuro-income" />
|
||||
<span className="font-medium text-lg">{selectedMonth}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="neuro-flat p-2 rounded-full"
|
||||
onClick={handleNextMonth}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
<div className="neuro-card">
|
||||
<p className="text-sm text-gray-500 mb-1">총 예산</p>
|
||||
<p className="text-lg font-bold text-neuro-income">
|
||||
{formatCurrency(budgetData?.monthly?.targetAmount || 0)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="neuro-card">
|
||||
<p className="text-sm text-gray-500 mb-1">총 지출</p>
|
||||
<p className="text-lg font-bold text-neuro-income">
|
||||
{formatCurrency(totalExpenses)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionsHeader;
|
||||
29
src/components/transactions/TransactionsList.tsx
Normal file
29
src/components/transactions/TransactionsList.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
import React from 'react';
|
||||
import TransactionCard, { Transaction } from '@/components/TransactionCard';
|
||||
import TransactionDateGroup from './TransactionDateGroup';
|
||||
|
||||
interface TransactionsListProps {
|
||||
groupedTransactions: Record<string, Transaction[]>;
|
||||
onTransactionDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
const TransactionsList: React.FC<TransactionsListProps> = ({
|
||||
groupedTransactions,
|
||||
onTransactionDelete
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-6 mb-[50px]">
|
||||
{Object.entries(groupedTransactions).map(([date, dateTransactions]) => (
|
||||
<TransactionDateGroup
|
||||
key={date}
|
||||
date={date}
|
||||
transactions={dateTransactions}
|
||||
onTransactionDelete={onTransactionDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionsList;
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
import { getPrevMonth, getNextMonth } from '../dateUtils';
|
||||
|
||||
/**
|
||||
* 월 선택 관련 훅
|
||||
* 이전/다음 월 선택 기능을 제공합니다.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import NavBar from '@/components/NavBar';
|
||||
import TransactionCard from '@/components/TransactionCard';
|
||||
import AddTransactionButton from '@/components/AddTransactionButton';
|
||||
import { Calendar, Search, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
|
||||
import { formatCurrency } from '@/utils/formatters';
|
||||
import { useTransactions, MONTHS_KR } from '@/hooks/transactions';
|
||||
import { useBudget } from '@/contexts/BudgetContext';
|
||||
import { useTransactions } from '@/hooks/transactions';
|
||||
import TransactionsHeader from '@/components/transactions/TransactionsHeader';
|
||||
import TransactionsContent from '@/components/transactions/TransactionsContent';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
|
||||
const Transactions = () => {
|
||||
const {
|
||||
@@ -32,17 +32,6 @@ const Transactions = () => {
|
||||
}
|
||||
}, [budgetData, isLoading]);
|
||||
|
||||
// 트랜잭션을 날짜별로 그룹화
|
||||
const groupedTransactions: Record<string, typeof transactions> = {};
|
||||
|
||||
transactions.forEach(transaction => {
|
||||
const datePart = transaction.date.split(',')[0];
|
||||
if (!groupedTransactions[datePart]) {
|
||||
groupedTransactions[datePart] = [];
|
||||
}
|
||||
groupedTransactions[datePart].push(transaction);
|
||||
});
|
||||
|
||||
// 트랜잭션 삭제 핸들러 (예외 처리 개선)
|
||||
const handleTransactionDelete = (id: string) => {
|
||||
try {
|
||||
@@ -88,122 +77,50 @@ const Transactions = () => {
|
||||
};
|
||||
}, [refreshTransactions]);
|
||||
|
||||
// 트랜잭션을 날짜별로 그룹화
|
||||
const groupTransactionsByDate = (transactions: Transaction[]): Record<string, Transaction[]> => {
|
||||
const grouped: Record<string, Transaction[]> = {};
|
||||
|
||||
transactions.forEach(transaction => {
|
||||
const datePart = transaction.date.split(',')[0];
|
||||
if (!grouped[datePart]) {
|
||||
grouped[datePart] = [];
|
||||
}
|
||||
grouped[datePart].push(transaction);
|
||||
});
|
||||
|
||||
return grouped;
|
||||
};
|
||||
|
||||
// 로딩이나 처리 중이면 비활성화된 UI 상태 표시
|
||||
const isDisabled = isLoading || isProcessing;
|
||||
const groupedTransactions = groupTransactionsByDate(transactions);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-neuro-background pb-24">
|
||||
<div className="max-w-md mx-auto px-6">
|
||||
{/* Header */}
|
||||
<header className="py-8">
|
||||
<h1 className="text-2xl font-bold neuro-text mb-5">지출 내역</h1>
|
||||
|
||||
{/* Search */}
|
||||
<div className="neuro-pressed mb-5 flex items-center px-4 py-3 rounded-xl">
|
||||
<Search size={18} className="text-gray-500 mr-2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="지출 검색..."
|
||||
className="bg-transparent flex-1 outline-none text-sm"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Month Selector */}
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<button
|
||||
className="neuro-flat p-2 rounded-full"
|
||||
onClick={handlePrevMonth}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={18} className="text-neuro-income" />
|
||||
<span className="font-medium text-lg">{selectedMonth}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="neuro-flat p-2 rounded-full"
|
||||
onClick={handleNextMonth}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
<div className="neuro-card">
|
||||
<p className="text-sm text-gray-500 mb-1">총 예산</p>
|
||||
<p className="text-lg font-bold text-neuro-income">
|
||||
{formatCurrency(budgetData?.monthly?.targetAmount || 0)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="neuro-card">
|
||||
<p className="text-sm text-gray-500 mb-1">총 지출</p>
|
||||
<p className="text-lg font-bold text-neuro-income">
|
||||
{formatCurrency(totalExpenses)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Loading or Processing State */}
|
||||
{(isLoading || isProcessing) && (
|
||||
<div className="flex justify-center items-center py-10">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-neuro-income" />
|
||||
<span className="ml-2 text-gray-500">{isProcessing ? '처리 중...' : '로딩 중...'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!isLoading && !isProcessing && transactions.length === 0 && (
|
||||
<div className="text-center py-10">
|
||||
<p className="text-gray-500 mb-3">
|
||||
{searchQuery.trim()
|
||||
? '검색 결과가 없습니다.'
|
||||
: `${selectedMonth}에 등록된 지출이 없습니다.`}
|
||||
</p>
|
||||
{searchQuery.trim() && (
|
||||
<button
|
||||
className="text-neuro-income"
|
||||
onClick={() => setSearchQuery('')}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
검색 초기화
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Transactions By Date */}
|
||||
{!isLoading && !isProcessing && transactions.length > 0 && (
|
||||
<div className="space-y-6 mb-[50px]">
|
||||
{Object.entries(groupedTransactions).map(([date, dateTransactions]) => (
|
||||
<div key={date}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="h-1 flex-1 neuro-pressed"></div>
|
||||
<h2 className="text-sm font-medium text-gray-500">{date}</h2>
|
||||
<div className="h-1 flex-1 neuro-pressed"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{dateTransactions.map(transaction => (
|
||||
<TransactionCard
|
||||
key={transaction.id}
|
||||
transaction={transaction}
|
||||
onDelete={handleTransactionDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<TransactionsHeader
|
||||
selectedMonth={selectedMonth}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
handlePrevMonth={handlePrevMonth}
|
||||
handleNextMonth={handleNextMonth}
|
||||
budgetData={budgetData}
|
||||
totalExpenses={totalExpenses}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
|
||||
<TransactionsContent
|
||||
isLoading={isLoading}
|
||||
isProcessing={isProcessing}
|
||||
transactions={transactions}
|
||||
groupedTransactions={groupedTransactions}
|
||||
searchQuery={searchQuery}
|
||||
selectedMonth={selectedMonth}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onTransactionDelete={handleTransactionDelete}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AddTransactionButton />
|
||||
|
||||
Reference in New Issue
Block a user