Implement code changes
The prompt asked to implement code changes.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
@@ -32,6 +32,9 @@ interface TransactionEditDialogProps {
|
||||
onDelete?: (id: string) => Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 트랜잭션 편집 다이얼로그 - 안정성 및 UX 개선 버전
|
||||
*/
|
||||
const TransactionEditDialog: React.FC<TransactionEditDialogProps> = ({
|
||||
transaction,
|
||||
open,
|
||||
@@ -40,9 +43,13 @@ const TransactionEditDialog: React.FC<TransactionEditDialogProps> = ({
|
||||
onDelete
|
||||
}) => {
|
||||
const { updateTransaction, deleteTransaction } = useBudget();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 작업 중첩 방지를 위한 참조
|
||||
const isProcessingRef = useRef(false);
|
||||
|
||||
// 폼 설정
|
||||
const form = useForm<TransactionFormValues>({
|
||||
resolver: zodResolver(transactionFormSchema),
|
||||
defaultValues: {
|
||||
@@ -51,46 +58,86 @@ const TransactionEditDialog: React.FC<TransactionEditDialogProps> = ({
|
||||
category: transaction.category as '식비' | '생활비' | '교통비',
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (values: TransactionFormValues) => {
|
||||
// Remove commas from amount string and convert to number
|
||||
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);
|
||||
|
||||
// 다이얼로그가 열릴 때 폼 값 초기화
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.reset({
|
||||
title: transaction.title,
|
||||
amount: formatWithCommas(transaction.amount.toString()),
|
||||
category: transaction.category as '식비' | '생활비' | '교통비',
|
||||
});
|
||||
}
|
||||
}, [open, transaction, form]);
|
||||
|
||||
// 저장 처리 함수
|
||||
const handleSubmit = async (values: TransactionFormValues) => {
|
||||
// 중복 제출 방지
|
||||
if (isProcessingRef.current) return;
|
||||
isProcessingRef.current = true;
|
||||
setIsSubmitting(true);
|
||||
|
||||
onOpenChange(false);
|
||||
|
||||
toast({
|
||||
title: "지출이 수정되었습니다",
|
||||
description: `${values.title} 항목이 ${formatWithCommas(cleanAmount)}원으로 수정되었습니다.`,
|
||||
});
|
||||
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 콜백이 있다면 호출
|
||||
// 부모 컴포넌트의 onDelete 콜백이 있다면 호출
|
||||
if (onDelete) {
|
||||
return await onDelete(transaction.id);
|
||||
const result = await onDelete(transaction.id);
|
||||
isProcessingRef.current = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 부모 컴포넌트에서 처리하지 않은 경우 기본 처리
|
||||
deleteTransaction(transaction.id);
|
||||
isProcessingRef.current = false;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('트랜잭션 삭제 중 오류:', error);
|
||||
@@ -99,12 +146,17 @@ const TransactionEditDialog: React.FC<TransactionEditDialogProps> = ({
|
||||
description: "지출 항목을 삭제하는데 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
isProcessingRef.current = false;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={(newOpen) => {
|
||||
// 제출 중이면 닫기 방지
|
||||
if (isSubmitting && !newOpen) return;
|
||||
onOpenChange(newOpen);
|
||||
}}>
|
||||
<DialogContent className={`sm:max-w-md mx-auto ${isMobile ? 'rounded-xl overflow-hidden' : ''}`}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>지출 수정</DialogTitle>
|
||||
@@ -121,13 +173,20 @@ const TransactionEditDialog: React.FC<TransactionEditDialogProps> = ({
|
||||
<TransactionDeleteAlert onDelete={handleDelete} />
|
||||
<div className="flex gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">취소</Button>
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Trash2, Loader2 } from 'lucide-react';
|
||||
import {
|
||||
@@ -18,30 +18,53 @@ interface TransactionDeleteAlertProps {
|
||||
onDelete: () => Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 트랜잭션 삭제 확인 다이얼로그 - 완전히 개선된 버전
|
||||
* 삭제 중복 방지, 상태 관리 개선, 메모리 누수 방지 로직 추가
|
||||
*/
|
||||
const TransactionDeleteAlert: React.FC<TransactionDeleteAlertProps> = ({ onDelete }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 타임아웃 참조 저장 (메모리 누수 방지용)
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// 클린업 함수 - 메모리 누수 방지
|
||||
const clearTimeouts = () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 컴포넌트 언마운트 시 모든 타임아웃 제거
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeouts();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDelete = async () => {
|
||||
// 이미 삭제 중이면 중복 실행 방지
|
||||
if (isDeleting) return;
|
||||
|
||||
try {
|
||||
if (isDeleting) return; // 중복 클릭 방지
|
||||
|
||||
// 삭제 상태 활성화
|
||||
setIsDeleting(true);
|
||||
|
||||
// 삭제 작업 시작 즉시 다이얼로그 닫기 (UI 응답성 향상)
|
||||
// 다이얼로그 즉시 닫기 (UI 응답성 개선)
|
||||
setIsOpen(false);
|
||||
|
||||
// 짧은 딜레이 추가 (UI 애니메이션 완료를 위해)
|
||||
setTimeout(async () => {
|
||||
// UI 애니메이션 완료 후 삭제 실행
|
||||
timeoutRef.current = setTimeout(async () => {
|
||||
try {
|
||||
// 삭제 함수 실행
|
||||
const result = await onDelete();
|
||||
console.log('삭제 결과:', result);
|
||||
await onDelete();
|
||||
} catch (error) {
|
||||
console.error('삭제 처리 오류:', error);
|
||||
} finally {
|
||||
// 상태 정리 (약간 지연)
|
||||
setTimeout(() => {
|
||||
// 모든 작업 완료 후 상태 초기화 (약간 지연)
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setIsDeleting(false);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import TransactionCard, { Transaction } from '@/components/TransactionCard';
|
||||
|
||||
interface TransactionDateGroupProps {
|
||||
@@ -8,22 +8,30 @@ interface TransactionDateGroupProps {
|
||||
onTransactionDelete: (id: string) => Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜별 트랜잭션 그룹 컴포넌트 - 성능 및 안정성 개선 버전
|
||||
*/
|
||||
const TransactionDateGroup: React.FC<TransactionDateGroupProps> = ({
|
||||
date,
|
||||
transactions,
|
||||
onTransactionDelete
|
||||
}) => {
|
||||
// 안정적인 삭제 핸들러
|
||||
const handleDelete = async (id: string): Promise<boolean> => {
|
||||
// 메모이즈된 삭제 핸들러로 성능 최적화
|
||||
const handleDelete = useCallback(async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
// 적절한 타입 변환 처리
|
||||
if (!onTransactionDelete) {
|
||||
console.warn('삭제 핸들러가 제공되지 않았습니다');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Promise 반환 여부에 따라 적절히 처리
|
||||
const result = await Promise.resolve(onTransactionDelete(id));
|
||||
return !!result;
|
||||
return Boolean(result);
|
||||
} catch (error) {
|
||||
console.error('삭제 처리 중 오류:', error);
|
||||
console.error('트랜잭션 삭제 처리 중 오류:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}, [onTransactionDelete]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -46,4 +54,4 @@ const TransactionDateGroup: React.FC<TransactionDateGroupProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionDateGroup;
|
||||
export default React.memo(TransactionDateGroup);
|
||||
|
||||
Reference in New Issue
Block a user