refactor(WEB): 공통 훅(useDeleteDialog, useStatsLoader) 및 CRUD 서비스 추출
- useDeleteDialog 훅 추출로 삭제 다이얼로그 로직 공통화 - useStatsLoader 훅 추출로 통계 로딩 패턴 공통화 - create-crud-service 유틸 추가 - 차량관리/견적/출고/검사 등 리스트 컴포넌트 간소화 - RankManagement actions 정리 - 프로덕션 로거 불필요 출력 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
* - 삭제/일괄삭제 다이얼로그
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useTransition } from 'react';
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format, startOfMonth, endOfMonth } from 'date-fns';
|
||||
import {
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
||||
import { StandardDialog } from '@/components/molecules/StandardDialog';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { useDeleteDialog } from '@/hooks/useDeleteDialog';
|
||||
import { toast } from 'sonner';
|
||||
import { formatAmount, formatAmountManwon } from '@/utils/formatAmount';
|
||||
import type { Quote, QuoteFilterType } from './types';
|
||||
@@ -69,7 +70,12 @@ export function QuoteManagementClient({
|
||||
initialPagination,
|
||||
}: QuoteManagementClientProps) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const deleteDialog = useDeleteDialog({
|
||||
onDelete: deleteQuote,
|
||||
onBulkDelete: bulkDeleteQuotes,
|
||||
onSuccess: () => window.location.reload(),
|
||||
entityName: '견적',
|
||||
});
|
||||
|
||||
// ===== 날짜 필터 상태 =====
|
||||
const today = new Date();
|
||||
@@ -84,12 +90,6 @@ export function QuoteManagementClient({
|
||||
const [isCalculationDialogOpen, setIsCalculationDialogOpen] = useState(false);
|
||||
const [calculationQuote, setCalculationQuote] = useState<Quote | null>(null);
|
||||
|
||||
// ===== 삭제 다이얼로그 상태 =====
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
const [bulkDeleteIds, setBulkDeleteIds] = useState<string[]>([]);
|
||||
|
||||
// ===== 전체 데이터 상태 (통계 계산용) =====
|
||||
const [allQuotes, setAllQuotes] = useState<Quote[]>(initialData);
|
||||
|
||||
@@ -102,57 +102,6 @@ export function QuoteManagementClient({
|
||||
router.push(`/sales/quote-management/${quote.id}?mode=edit`);
|
||||
}, [router]);
|
||||
|
||||
const handleDeleteClick = useCallback((id: string) => {
|
||||
setDeleteTargetId(id);
|
||||
setIsDeleteDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (!deleteTargetId) return;
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await deleteQuote(deleteTargetId);
|
||||
|
||||
if (result.success) {
|
||||
const quote = allQuotes.find((q) => q.id === deleteTargetId);
|
||||
setAllQuotes(allQuotes.filter((q) => q.id !== deleteTargetId));
|
||||
toast.success(`견적이 삭제되었습니다${quote ? `: ${quote.quoteNumber}` : ''}`);
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
}
|
||||
|
||||
setIsDeleteDialogOpen(false);
|
||||
setDeleteTargetId(null);
|
||||
});
|
||||
}, [deleteTargetId, allQuotes]);
|
||||
|
||||
const handleBulkDelete = useCallback((selectedIds: string[]) => {
|
||||
if (selectedIds.length === 0) {
|
||||
toast.error('삭제할 항목을 선택해주세요');
|
||||
return;
|
||||
}
|
||||
setBulkDeleteIds(selectedIds);
|
||||
setIsBulkDeleteDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmBulkDelete = useCallback(async () => {
|
||||
startTransition(async () => {
|
||||
const result = await bulkDeleteQuotes(bulkDeleteIds);
|
||||
|
||||
if (result.success) {
|
||||
setAllQuotes(allQuotes.filter((q) => !bulkDeleteIds.includes(q.id)));
|
||||
toast.success(`${bulkDeleteIds.length}개의 견적이 삭제되었습니다`);
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error(result.error || '일괄 삭제에 실패했습니다.');
|
||||
}
|
||||
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setBulkDeleteIds([]);
|
||||
});
|
||||
}, [bulkDeleteIds, allQuotes]);
|
||||
|
||||
const handleViewHistory = useCallback((quote: Quote) => {
|
||||
toast.info(`수정 이력: ${quote.quoteNumber} (${quote.currentRevision}차 수정)`);
|
||||
}, []);
|
||||
@@ -413,7 +362,7 @@ export function QuoteManagementClient({
|
||||
),
|
||||
|
||||
// 일괄 삭제 핸들러
|
||||
onBulkDelete: handleBulkDelete,
|
||||
onBulkDelete: deleteDialog.bulk.open,
|
||||
|
||||
// 테이블 행 렌더링
|
||||
renderTableRow: (
|
||||
@@ -489,8 +438,8 @@ export function QuoteManagementClient({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteClick(quote.id)}
|
||||
disabled={isPending}
|
||||
onClick={() => deleteDialog.single.open(quote.id)}
|
||||
disabled={deleteDialog.isPending}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
@@ -585,9 +534,9 @@ export function QuoteManagementClient({
|
||||
className="flex-1 min-w-[100px] h-11 border-red-200 text-red-600 hover:border-red-300 bg-transparent"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick(quote.id);
|
||||
deleteDialog.single.open(quote.id);
|
||||
}}
|
||||
disabled={isPending}
|
||||
disabled={deleteDialog.isPending}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
@@ -600,7 +549,7 @@ export function QuoteManagementClient({
|
||||
);
|
||||
},
|
||||
}),
|
||||
[computeStats, router, handleView, handleEdit, handleDeleteClick, handleViewHistory, handleBulkDelete, getRevisionBadge, isPending, startDate, endDate, productCategoryFilter, statusFilter]
|
||||
[computeStats, router, handleView, handleEdit, handleViewHistory, getRevisionBadge, deleteDialog, startDate, endDate, productCategoryFilter, statusFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -735,34 +684,28 @@ export function QuoteManagementClient({
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
open={deleteDialog.single.isOpen}
|
||||
onOpenChange={deleteDialog.single.onOpenChange}
|
||||
description={
|
||||
<>
|
||||
{deleteTargetId
|
||||
? `견적번호: ${allQuotes.find((q) => q.id === deleteTargetId)?.quoteNumber || deleteTargetId}`
|
||||
{deleteDialog.single.targetId
|
||||
? `견적번호: ${allQuotes.find((q) => q.id === deleteDialog.single.targetId)?.quoteNumber || deleteDialog.single.targetId}`
|
||||
: ''}
|
||||
<br />
|
||||
이 견적을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
loading={isPending}
|
||||
onConfirm={handleConfirmDelete}
|
||||
loading={deleteDialog.isPending}
|
||||
onConfirm={deleteDialog.single.confirm}
|
||||
/>
|
||||
|
||||
{/* 일괄 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
open={isBulkDeleteDialogOpen}
|
||||
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||
description={
|
||||
<>
|
||||
선택한 {bulkDeleteIds.length}개의 견적을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 데이터는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
loading={isPending}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
open={deleteDialog.bulk.isOpen}
|
||||
onOpenChange={deleteDialog.bulk.onOpenChange}
|
||||
description={`선택한 ${deleteDialog.bulk.ids.length}개의 견적을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.`}
|
||||
loading={deleteDialog.isPending}
|
||||
onConfirm={deleteDialog.bulk.confirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user