Refactor TransactionEditDialog component
The TransactionEditDialog component was refactored into smaller, more manageable components to improve code organization and maintainability. The functionality remains the same.
This commit is contained in:
@@ -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,
|
||||
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<TransactionEditDialogProps> = ({
|
||||
transaction,
|
||||
@@ -43,123 +30,14 @@ const TransactionEditDialog: React.FC<TransactionEditDialogProps> = ({
|
||||
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<TransactionFormValues>({
|
||||
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<boolean> => {
|
||||
// 중복 처리 방지
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={(newOpen) => {
|
||||
@@ -175,33 +53,12 @@ const TransactionEditDialog: React.FC<TransactionEditDialogProps> = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
<TransactionFormFields form={form} />
|
||||
|
||||
<DialogFooter className="flex justify-between gap-2 mt-6">
|
||||
<TransactionDeleteAlert onDelete={handleDelete} />
|
||||
<div className="flex gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-neuro-income text-white hover:bg-neuro-income/90"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '저장 중...' : '저장'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
<TransactionEditForm
|
||||
form={form}
|
||||
onSubmit={handleSubmit}
|
||||
onDelete={handleDelete}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
57
src/components/transaction/TransactionEditForm.tsx
Normal file
57
src/components/transaction/TransactionEditForm.tsx
Normal file
@@ -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<TransactionFormValues>;
|
||||
onSubmit: (values: TransactionFormValues) => void;
|
||||
onDelete: () => Promise<boolean>;
|
||||
isSubmitting: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 트랜잭션 편집 폼 컴포넌트 - UI 부분만 분리
|
||||
*/
|
||||
const TransactionEditForm: React.FC<TransactionEditFormProps> = ({
|
||||
form,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
isSubmitting
|
||||
}) => {
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<TransactionFormFields form={form} />
|
||||
|
||||
<DialogFooter className="flex justify-between gap-2 mt-6">
|
||||
<TransactionDeleteAlert onDelete={onDelete} />
|
||||
<div className="flex gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-neuro-income text-white hover:bg-neuro-income/90"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '저장 중...' : '저장'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionEditForm;
|
||||
13
src/components/transaction/categoryUtils.ts
Normal file
13
src/components/transaction/categoryUtils.ts
Normal file
@@ -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 '쇼핑';
|
||||
};
|
||||
135
src/components/transaction/useTransactionEdit.ts
Normal file
135
src/components/transaction/useTransactionEdit.ts
Normal file
@@ -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> | boolean,
|
||||
) => {
|
||||
const { updateTransaction, deleteTransaction } = useBudget();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 작업 중첩 방지를 위한 참조
|
||||
const isProcessingRef = useRef(false);
|
||||
|
||||
// 폼 설정
|
||||
const form = useForm<TransactionFormValues>({
|
||||
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<boolean> => {
|
||||
// 중복 처리 방지
|
||||
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
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user