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:
유병철
2026-02-09 20:42:05 +09:00
parent 3ea6a57a10
commit 4d79b178e3
22 changed files with 588 additions and 619 deletions

View File

@@ -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}
/>
</>
);