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:
gpt-engineer-app[bot]
2025-03-17 23:24:12 +00:00
parent dea8b9f8ba
commit f1f9227abf
7 changed files with 296 additions and 126 deletions

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -1,4 +1,6 @@
import { getPrevMonth, getNextMonth } from '../dateUtils';
/**
* 월 선택 관련 훅
* 이전/다음 월 선택 기능을 제공합니다.

View File

@@ -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>
<TransactionsHeader
selectedMonth={selectedMonth}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
handlePrevMonth={handlePrevMonth}
handleNextMonth={handleNextMonth}
budgetData={budgetData}
totalExpenses={totalExpenses}
isDisabled={isDisabled}
/>
{/* 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>
)}
<TransactionsContent
isLoading={isLoading}
isProcessing={isProcessing}
transactions={transactions}
groupedTransactions={groupedTransactions}
searchQuery={searchQuery}
selectedMonth={selectedMonth}
setSearchQuery={setSearchQuery}
onTransactionDelete={handleTransactionDelete}
isDisabled={isDisabled}
/>
</div>
<AddTransactionButton />