From c473abda72d973d9d0cb039a2e7702d7c725ac88 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:05:01 +0000 Subject: [PATCH] Refactor TransactionEditDialog component The TransactionEditDialog component was refactored into smaller, more manageable components to improve code organization and maintainability. The functionality remains the same. --- src/components/TransactionEditDialog.tsx | 179 ++---------------- .../transaction/TransactionEditForm.tsx | 57 ++++++ src/components/transaction/categoryUtils.ts | 13 ++ .../transaction/useTransactionEdit.ts | 135 +++++++++++++ 4 files changed, 223 insertions(+), 161 deletions(-) create mode 100644 src/components/transaction/TransactionEditForm.tsx create mode 100644 src/components/transaction/categoryUtils.ts create mode 100644 src/components/transaction/useTransactionEdit.ts diff --git a/src/components/TransactionEditDialog.tsx b/src/components/TransactionEditDialog.tsx index 2d613c7..05a4fd6 100644 --- a/src/components/TransactionEditDialog.tsx +++ b/src/components/TransactionEditDialog.tsx @@ -1,29 +1,16 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; +import React from 'react'; import { Transaction } from '@/components/TransactionCard'; -import { useBudget } from '@/contexts/BudgetContext'; import { Dialog, DialogContent, DialogHeader, - DialogTitle, - DialogFooter, - DialogClose, + DialogTitle, DialogDescription } from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Form } from '@/components/ui/form'; -import { toast } from '@/components/ui/use-toast'; -import TransactionFormFields, { - transactionFormSchema, - formatWithCommas, - TransactionFormValues -} from './transaction/TransactionFormFields'; -import TransactionDeleteAlert from './transaction/TransactionDeleteAlert'; import { useIsMobile } from '@/hooks/use-mobile'; -import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; +import { useTransactionEdit } from './transaction/useTransactionEdit'; +import TransactionEditForm from './transaction/TransactionEditForm'; interface TransactionEditDialogProps { transaction: Transaction; @@ -34,7 +21,7 @@ interface TransactionEditDialogProps { } /** - * 트랜잭션 편집 다이얼로그 - 안정성 및 UX 개선 버전 + * 트랜잭션 편집 다이얼로그 - 리팩토링 후 간소화된 버전 */ const TransactionEditDialog: React.FC = ({ transaction, @@ -43,123 +30,14 @@ const TransactionEditDialog: React.FC = ({ onSave, onDelete }) => { - const { updateTransaction, deleteTransaction } = useBudget(); const isMobile = useIsMobile(); - const [isSubmitting, setIsSubmitting] = useState(false); - - // 작업 중첩 방지를 위한 참조 - const isProcessingRef = useRef(false); - - // 카테고리 매핑 함수 - 이전 카테고리명을 새 카테고리명으로 변환 - const mapCategoryToNew = (oldCategory: string): "음식" | "쇼핑" | "교통비" => { - if (oldCategory === '식비') return '음식'; - if (oldCategory === '생활비') return '쇼핑'; - if (EXPENSE_CATEGORIES.includes(oldCategory as any)) return oldCategory as "음식" | "쇼핑" | "교통비"; - // 기본값은 '쇼핑'으로 설정 - return '쇼핑'; - }; - - // 폼 설정 - const form = useForm({ - resolver: zodResolver(transactionFormSchema), - defaultValues: { - title: transaction.title, - amount: formatWithCommas(transaction.amount.toString()), - category: mapCategoryToNew(transaction.category), - }, - }); - - // 다이얼로그가 열릴 때 폼 값 초기화 - useEffect(() => { - if (open) { - form.reset({ - title: transaction.title, - amount: formatWithCommas(transaction.amount.toString()), - category: mapCategoryToNew(transaction.category), - }); - } - }, [open, transaction, form]); - - // 저장 처리 함수 - const handleSubmit = async (values: TransactionFormValues) => { - // 중복 제출 방지 - if (isProcessingRef.current) return; - isProcessingRef.current = true; - setIsSubmitting(true); - - try { - // 쉼표 제거 및 숫자로 변환 - const cleanAmount = values.amount.replace(/,/g, ''); - - const updatedTransaction = { - ...transaction, - title: values.title, - amount: Number(cleanAmount), - category: values.category, - }; - - // 컨텍스트를 통해 트랜잭션 업데이트 - updateTransaction(updatedTransaction); - - // 부모 컴포넌트의 onSave 콜백이 있다면 호출 - if (onSave) { - onSave(updatedTransaction); - } - - // 다이얼로그 닫기 - onOpenChange(false); - - // 토스트 메시지 - toast({ - title: "지출이 수정되었습니다", - description: `${values.title} 항목이 ${formatWithCommas(cleanAmount)}원으로 수정되었습니다.`, - }); - } catch (error) { - console.error('트랜잭션 업데이트 오류:', error); - toast({ - title: "저장 실패", - description: "지출 항목을 저장하는데 문제가 발생했습니다.", - variant: "destructive" - }); - } finally { - // 상태 초기화 - setIsSubmitting(false); - isProcessingRef.current = false; - } - }; - - // 삭제 처리 함수 - const handleDelete = async (): Promise => { - // 중복 처리 방지 - if (isProcessingRef.current) return false; - isProcessingRef.current = true; - - try { - // 다이얼로그 닫기를 먼저 수행 (UI 블로킹 방지) - onOpenChange(false); - - // 부모 컴포넌트의 onDelete 콜백이 있다면 호출 - if (onDelete) { - const result = await onDelete(transaction.id); - isProcessingRef.current = false; - return result; - } - - // 부모 컴포넌트에서 처리하지 않은 경우 기본 처리 - deleteTransaction(transaction.id); - isProcessingRef.current = false; - return true; - } catch (error) { - console.error('트랜잭션 삭제 중 오류:', error); - toast({ - title: "삭제 실패", - description: "지출 항목을 삭제하는데 문제가 발생했습니다.", - variant: "destructive" - }); - isProcessingRef.current = false; - return false; - } - }; + const { form, isSubmitting, handleSubmit, handleDelete } = useTransactionEdit( + transaction, + open, + onOpenChange, + onSave, + onDelete + ); return ( { @@ -175,33 +53,12 @@ const TransactionEditDialog: React.FC = ({ -
- - - - - -
- - - - -
-
- - +
); diff --git a/src/components/transaction/TransactionEditForm.tsx b/src/components/transaction/TransactionEditForm.tsx new file mode 100644 index 0000000..204f5a7 --- /dev/null +++ b/src/components/transaction/TransactionEditForm.tsx @@ -0,0 +1,57 @@ + +import React from 'react'; +import { Form } from '@/components/ui/form'; +import { Button } from '@/components/ui/button'; +import { DialogClose, DialogFooter } from '@/components/ui/dialog'; +import TransactionFormFields, { TransactionFormValues } from './TransactionFormFields'; +import TransactionDeleteAlert from './TransactionDeleteAlert'; +import { UseFormReturn } from 'react-hook-form'; + +interface TransactionEditFormProps { + form: UseFormReturn; + onSubmit: (values: TransactionFormValues) => void; + onDelete: () => Promise; + isSubmitting: boolean; +} + +/** + * 트랜잭션 편집 폼 컴포넌트 - UI 부분만 분리 + */ +const TransactionEditForm: React.FC = ({ + form, + onSubmit, + onDelete, + isSubmitting +}) => { + return ( +
+ + + + + +
+ + + + +
+
+ + + ); +}; + +export default TransactionEditForm; diff --git a/src/components/transaction/categoryUtils.ts b/src/components/transaction/categoryUtils.ts new file mode 100644 index 0000000..87a9eb7 --- /dev/null +++ b/src/components/transaction/categoryUtils.ts @@ -0,0 +1,13 @@ + +import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons'; + +/** + * 카테고리 매핑 함수 - 이전 카테고리명을 새 카테고리명으로 변환 + */ +export const mapCategoryToNew = (oldCategory: string): "음식" | "쇼핑" | "교통비" => { + if (oldCategory === '식비') return '음식'; + if (oldCategory === '생활비') return '쇼핑'; + if (EXPENSE_CATEGORIES.includes(oldCategory as any)) return oldCategory as "음식" | "쇼핑" | "교통비"; + // 기본값은 '쇼핑'으로 설정 + return '쇼핑'; +}; diff --git a/src/components/transaction/useTransactionEdit.ts b/src/components/transaction/useTransactionEdit.ts new file mode 100644 index 0000000..fa8dfbd --- /dev/null +++ b/src/components/transaction/useTransactionEdit.ts @@ -0,0 +1,135 @@ + +import { useState, useRef, useEffect } from 'react'; +import { UseFormReturn, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Transaction } from '@/components/TransactionCard'; +import { useBudget } from '@/contexts/BudgetContext'; +import { toast } from '@/components/ui/use-toast'; +import { TransactionFormValues, transactionFormSchema, formatWithCommas } from './TransactionFormFields'; +import { mapCategoryToNew } from './categoryUtils'; + +/** + * 트랜잭션 편집 커스텀 훅 - 상태 및 핸들러 로직 분리 + */ +export const useTransactionEdit = ( + transaction: Transaction, + open: boolean, + onOpenChange: (open: boolean) => void, + onSave?: (updatedTransaction: Transaction) => void, + onDelete?: (id: string) => Promise | boolean, +) => { + const { updateTransaction, deleteTransaction } = useBudget(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // 작업 중첩 방지를 위한 참조 + const isProcessingRef = useRef(false); + + // 폼 설정 + const form = useForm({ + resolver: zodResolver(transactionFormSchema), + defaultValues: { + title: transaction.title, + amount: formatWithCommas(transaction.amount.toString()), + category: mapCategoryToNew(transaction.category), + }, + }); + + // 다이얼로그가 열릴 때 폼 값 초기화 + useEffect(() => { + if (open) { + form.reset({ + title: transaction.title, + amount: formatWithCommas(transaction.amount.toString()), + category: mapCategoryToNew(transaction.category), + }); + } + }, [open, transaction, form]); + + // 저장 처리 함수 + const handleSubmit = async (values: TransactionFormValues) => { + // 중복 제출 방지 + if (isProcessingRef.current) return; + isProcessingRef.current = true; + setIsSubmitting(true); + + try { + // 쉼표 제거 및 숫자로 변환 + const cleanAmount = values.amount.replace(/,/g, ''); + + const updatedTransaction = { + ...transaction, + title: values.title, + amount: Number(cleanAmount), + category: values.category, + }; + + // 컨텍스트를 통해 트랜잭션 업데이트 + updateTransaction(updatedTransaction); + + // 부모 컴포넌트의 onSave 콜백이 있다면 호출 + if (onSave) { + onSave(updatedTransaction); + } + + // 다이얼로그 닫기 + onOpenChange(false); + + // 토스트 메시지 + toast({ + title: "지출이 수정되었습니다", + description: `${values.title} 항목이 ${formatWithCommas(cleanAmount)}원으로 수정되었습니다.`, + }); + } catch (error) { + console.error('트랜잭션 업데이트 오류:', error); + toast({ + title: "저장 실패", + description: "지출 항목을 저장하는데 문제가 발생했습니다.", + variant: "destructive" + }); + } finally { + // 상태 초기화 + setIsSubmitting(false); + isProcessingRef.current = false; + } + }; + + // 삭제 처리 함수 + const handleDelete = async (): Promise => { + // 중복 처리 방지 + if (isProcessingRef.current) return false; + isProcessingRef.current = true; + + try { + // 다이얼로그 닫기를 먼저 수행 (UI 블로킹 방지) + onOpenChange(false); + + // 부모 컴포넌트의 onDelete 콜백이 있다면 호출 + if (onDelete) { + const result = await onDelete(transaction.id); + isProcessingRef.current = false; + return result; + } + + // 부모 컴포넌트에서 처리하지 않은 경우 기본 처리 + deleteTransaction(transaction.id); + isProcessingRef.current = false; + return true; + } catch (error) { + console.error('트랜잭션 삭제 중 오류:', error); + toast({ + title: "삭제 실패", + description: "지출 항목을 삭제하는데 문제가 발생했습니다.", + variant: "destructive" + }); + isProcessingRef.current = false; + return false; + } + }; + + return { + form, + isSubmitting, + handleSubmit, + handleDelete + }; +};