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 NavBar from '@/components/NavBar';
|
||||||
import TransactionCard from '@/components/TransactionCard';
|
|
||||||
import AddTransactionButton from '@/components/AddTransactionButton';
|
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 { 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 Transactions = () => {
|
||||||
const {
|
const {
|
||||||
@@ -32,17 +32,6 @@ const Transactions = () => {
|
|||||||
}
|
}
|
||||||
}, [budgetData, isLoading]);
|
}, [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) => {
|
const handleTransactionDelete = (id: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -88,122 +77,50 @@ const Transactions = () => {
|
|||||||
};
|
};
|
||||||
}, [refreshTransactions]);
|
}, [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 상태 표시
|
// 로딩이나 처리 중이면 비활성화된 UI 상태 표시
|
||||||
const isDisabled = isLoading || isProcessing;
|
const isDisabled = isLoading || isProcessing;
|
||||||
|
const groupedTransactions = groupTransactionsByDate(transactions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neuro-background pb-24">
|
<div className="min-h-screen bg-neuro-background pb-24">
|
||||||
<div className="max-w-md mx-auto px-6">
|
<div className="max-w-md mx-auto px-6">
|
||||||
{/* Header */}
|
<TransactionsHeader
|
||||||
<header className="py-8">
|
selectedMonth={selectedMonth}
|
||||||
<h1 className="text-2xl font-bold neuro-text mb-5">지출 내역</h1>
|
searchQuery={searchQuery}
|
||||||
|
setSearchQuery={setSearchQuery}
|
||||||
|
handlePrevMonth={handlePrevMonth}
|
||||||
|
handleNextMonth={handleNextMonth}
|
||||||
|
budgetData={budgetData}
|
||||||
|
totalExpenses={totalExpenses}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Search */}
|
<TransactionsContent
|
||||||
<div className="neuro-pressed mb-5 flex items-center px-4 py-3 rounded-xl">
|
isLoading={isLoading}
|
||||||
<Search size={18} className="text-gray-500 mr-2" />
|
isProcessing={isProcessing}
|
||||||
<input
|
transactions={transactions}
|
||||||
type="text"
|
groupedTransactions={groupedTransactions}
|
||||||
placeholder="지출 검색..."
|
searchQuery={searchQuery}
|
||||||
className="bg-transparent flex-1 outline-none text-sm"
|
selectedMonth={selectedMonth}
|
||||||
value={searchQuery}
|
setSearchQuery={setSearchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onTransactionDelete={handleTransactionDelete}
|
||||||
disabled={isDisabled}
|
isDisabled={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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddTransactionButton />
|
<AddTransactionButton />
|
||||||
|
|||||||
Reference in New Issue
Block a user