From 6015cbfe1bf4a5bff7f1f356be726b5cf94a226f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 22 Mar 2025 12:29:20 +0000 Subject: [PATCH] Refactor ExpenseFormFields component Refactor ExpenseFormFields.tsx into smaller, more manageable components for better organization and maintainability. --- .../expenses/ExpenseAmountInput.tsx | 51 ++++++ .../expenses/ExpenseCategorySelector.tsx | 6 +- src/components/expenses/ExpenseFormFields.tsx | 172 ++++-------------- .../expenses/ExpensePaymentMethod.tsx | 67 +++++++ src/components/expenses/ExpenseTitleInput.tsx | 35 ++++ .../expenses/ExpenseTitleSuggestions.tsx | 55 ++++++ 6 files changed, 248 insertions(+), 138 deletions(-) create mode 100644 src/components/expenses/ExpenseAmountInput.tsx create mode 100644 src/components/expenses/ExpensePaymentMethod.tsx create mode 100644 src/components/expenses/ExpenseTitleInput.tsx create mode 100644 src/components/expenses/ExpenseTitleSuggestions.tsx diff --git a/src/components/expenses/ExpenseAmountInput.tsx b/src/components/expenses/ExpenseAmountInput.tsx new file mode 100644 index 0000000..95086a3 --- /dev/null +++ b/src/components/expenses/ExpenseAmountInput.tsx @@ -0,0 +1,51 @@ + +import React from 'react'; +import { FormField, FormItem, FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { UseFormReturn } from 'react-hook-form'; +import { ExpenseFormValues } from './ExpenseForm'; + +interface ExpenseAmountInputProps { + form: UseFormReturn; + onFocus?: () => void; + isDisabled?: boolean; +} + +const ExpenseAmountInput: React.FC = ({ + form, + onFocus, + isDisabled = false +}) => { + // Format number with commas + const formatWithCommas = (value: string): string => { + // Remove commas first to avoid duplicates when typing + const numericValue = value.replace(/[^0-9]/g, ''); + return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + }; + + const handleAmountChange = (e: React.ChangeEvent) => { + const formattedValue = formatWithCommas(e.target.value); + form.setValue('amount', formattedValue); + }; + + return ( + ( + + 금액 + + + )} + /> + ); +}; + +export default ExpenseAmountInput; diff --git a/src/components/expenses/ExpenseCategorySelector.tsx b/src/components/expenses/ExpenseCategorySelector.tsx index 263a405..2114f4f 100644 --- a/src/components/expenses/ExpenseCategorySelector.tsx +++ b/src/components/expenses/ExpenseCategorySelector.tsx @@ -7,11 +7,13 @@ import { categoryIcons, EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; interface ExpenseCategorySelectorProps { value: string; onValueChange: (value: string) => void; + isDisabled?: boolean; } const ExpenseCategorySelector: React.FC = ({ value, - onValueChange + onValueChange, + isDisabled = false }) => { return ( @@ -22,12 +24,14 @@ const ExpenseCategorySelector: React.FC = ({ onValueChange={(value) => { if (value) onValueChange(value); }} + disabled={isDisabled} > {EXPENSE_CATEGORIES.map((category) => (
{categoryIcons[category]} diff --git a/src/components/expenses/ExpenseFormFields.tsx b/src/components/expenses/ExpenseFormFields.tsx index 56d75e4..f1007a5 100644 --- a/src/components/expenses/ExpenseFormFields.tsx +++ b/src/components/expenses/ExpenseFormFields.tsx @@ -1,21 +1,22 @@ import React, { useEffect, useState } from 'react'; -import { useForm, UseFormReturn } from 'react-hook-form'; -import { FormField, FormItem, FormLabel } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { CreditCard, Banknote } from 'lucide-react'; -import ExpenseCategorySelector from './ExpenseCategorySelector'; -import { Badge } from '@/components/ui/badge'; -import { getPersonalizedTitleSuggestions } from '@/utils/userTitlePreferences'; -import { Separator } from '@/components/ui/separator'; +import { UseFormReturn } from 'react-hook-form'; import { ExpenseFormValues } from './ExpenseForm'; +import ExpenseCategorySelector from './ExpenseCategorySelector'; +import ExpenseTitleSuggestions from './ExpenseTitleSuggestions'; +import ExpenseTitleInput from './ExpenseTitleInput'; +import ExpenseAmountInput from './ExpenseAmountInput'; +import ExpensePaymentMethod from './ExpensePaymentMethod'; interface ExpenseFormFieldsProps { form: UseFormReturn; isSubmitting?: boolean; } -const ExpenseFormFields: React.FC = ({ form, isSubmitting = false }) => { +const ExpenseFormFields: React.FC = ({ + form, + isSubmitting = false +}) => { // 상태 관리 추가 const [showTitleSuggestions, setShowTitleSuggestions] = useState(false); const [showPaymentMethod, setShowPaymentMethod] = useState(false); @@ -23,14 +24,9 @@ const ExpenseFormFields: React.FC = ({ form, isSubmittin // 현재 선택된 카테고리 가져오기 const selectedCategory = form.watch('category'); - // 선택된 카테고리에 대한 개인화된 제목 제안 목록 상태 - const [titleSuggestions, setTitleSuggestions] = useState([]); - - // 카테고리가 변경될 때마다 개인화된 제목 목록 업데이트 및 제목 추천 표시 + // 카테고리가 변경될 때마다 제목 추천 표시 useEffect(() => { if (selectedCategory) { - const suggestions = getPersonalizedTitleSuggestions(selectedCategory); - setTitleSuggestions(suggestions); // 약간의 지연 후 제목 추천 표시 (애니메이션을 위해) setTimeout(() => { setShowTitleSuggestions(true); @@ -45,139 +41,41 @@ const ExpenseFormFields: React.FC = ({ form, isSubmittin form.setValue('title', suggestion); }; - // Format number with commas - const formatWithCommas = (value: string): string => { - // Remove commas first to avoid duplicates when typing - const numericValue = value.replace(/[^0-9]/g, ''); - return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ','); - }; - - const handleAmountChange = (e: React.ChangeEvent) => { - const formattedValue = formatWithCommas(e.target.value); - form.setValue('amount', formattedValue); - }; - return ( <> {/* 카테고리 필드를 가장 먼저 배치 */} - ( - - 카테고리 - field.onChange(value)} - /> - - )} + form.setValue('category', value)} + isDisabled={isSubmitting} /> - {/* 카테고리별 제목 제안 - 카테고리 선택 후에만 표시, 서랍처럼 열리는 애니메이션 적용 */} - {selectedCategory && ( -
- {titleSuggestions.length > 0 && ( -
- {titleSuggestions.map((suggestion) => ( - handleTitleSuggestionClick(suggestion)} - > - {suggestion} - - ))} -
- )} -
- )} + {/* 카테고리별 제목 제안 - 카테고리 선택 후에만 표시 */} + {/* 제목 필드를 두 번째로 배치 */} - ( - - 제목 - - - )} + {/* 금액 필드를 세 번째로 배치 */} - ( - - 금액 - setShowPaymentMethod(true)} - disabled={isSubmitting} - /> - - )} + setShowPaymentMethod(true)} + isDisabled={isSubmitting} + /> + + {/* 지출 방법 필드는 금액 입력 시에만 표시 */} + - - {/* 지출 방법 필드는 금액 입력 시에만 표시 - 서랍처럼 열리는 애니메이션 적용 */} -
- - - ( - - 지출 방법 -
-
!isSubmitting && form.setValue('paymentMethod', '신용카드')} - > - - 신용카드 -
-
!isSubmitting && form.setValue('paymentMethod', '현금')} - > - - 현금 -
-
-
- )} - /> -
); }; diff --git a/src/components/expenses/ExpensePaymentMethod.tsx b/src/components/expenses/ExpensePaymentMethod.tsx new file mode 100644 index 0000000..ea11a28 --- /dev/null +++ b/src/components/expenses/ExpensePaymentMethod.tsx @@ -0,0 +1,67 @@ + +import React from 'react'; +import { FormField, FormItem, FormLabel } from '@/components/ui/form'; +import { CreditCard, Banknote } from 'lucide-react'; +import { Separator } from '@/components/ui/separator'; +import { UseFormReturn } from 'react-hook-form'; +import { ExpenseFormValues } from './ExpenseForm'; + +interface ExpensePaymentMethodProps { + form: UseFormReturn; + showPaymentMethod: boolean; + isDisabled?: boolean; +} + +const ExpensePaymentMethod: React.FC = ({ + form, + showPaymentMethod, + isDisabled = false +}) => { + return ( +
+ + + ( + + 지출 방법 +
+
!isDisabled && form.setValue('paymentMethod', '신용카드')} + > + + 신용카드 +
+
!isDisabled && form.setValue('paymentMethod', '현금')} + > + + 현금 +
+
+
+ )} + /> +
+ ); +}; + +export default ExpensePaymentMethod; diff --git a/src/components/expenses/ExpenseTitleInput.tsx b/src/components/expenses/ExpenseTitleInput.tsx new file mode 100644 index 0000000..0765619 --- /dev/null +++ b/src/components/expenses/ExpenseTitleInput.tsx @@ -0,0 +1,35 @@ + +import React from 'react'; +import { FormField, FormItem, FormLabel } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { UseFormReturn } from 'react-hook-form'; +import { ExpenseFormValues } from './ExpenseForm'; + +interface ExpenseTitleInputProps { + form: UseFormReturn; + isDisabled?: boolean; +} + +const ExpenseTitleInput: React.FC = ({ + form, + isDisabled = false +}) => { + return ( + ( + + 제목 + + + )} + /> + ); +}; + +export default ExpenseTitleInput; diff --git a/src/components/expenses/ExpenseTitleSuggestions.tsx b/src/components/expenses/ExpenseTitleSuggestions.tsx new file mode 100644 index 0000000..ec5110a --- /dev/null +++ b/src/components/expenses/ExpenseTitleSuggestions.tsx @@ -0,0 +1,55 @@ + +import React, { useEffect, useState } from 'react'; +import { Badge } from '@/components/ui/badge'; +import { getPersonalizedTitleSuggestions } from '@/utils/userTitlePreferences'; + +interface ExpenseTitleSuggestionsProps { + category: string; + showSuggestions: boolean; + onSuggestionClick: (suggestion: string) => void; +} + +const ExpenseTitleSuggestions: React.FC = ({ + category, + showSuggestions, + onSuggestionClick +}) => { + const [titleSuggestions, setTitleSuggestions] = useState([]); + + // 카테고리가 변경될 때마다 개인화된 제목 목록 업데이트 + useEffect(() => { + if (category) { + const suggestions = getPersonalizedTitleSuggestions(category); + setTitleSuggestions(suggestions); + } + }, [category]); + + if (!category || titleSuggestions.length === 0) { + return null; + } + + return ( +
+
+ {titleSuggestions.map((suggestion) => ( + onSuggestionClick(suggestion)} + > + {suggestion} + + ))} +
+
+ ); +}; + +export default ExpenseTitleSuggestions;