feat(WEB): Phase 6 IntegratedDetailTemplate 마이그레이션 완료
Phase 6 마이그레이션 (41개 컴포넌트 완료): - 건설/시공: 협력업체, 시공관리, 기성관리, 발주관리, 계약관리 등 - 영업: 견적관리(V2), 고객관리(V2), 수주관리 - 회계: 청구관리, 매입관리, 매출관리, 거래처관리, 악성채권 등 - 생산: 작업지시, 검수관리 - 출고: 출하관리 - 자재: 입고관리, 재고현황 - 고객센터: 문의관리, 이벤트관리, 공지관리 - 인사: 직원관리 - 설정: 권한관리 주요 변경사항: - 34개 xxxConfig.ts 파일 생성 (설정 기반 페이지 구성) - PageLayout/PageHeader → IntegratedDetailTemplate 통합 - 일관된 타이틀/버튼 영역 (목록, 상세, 수정, 삭제) - 1112줄 코드 감소 (중복 제거) 프로젝트 공통화 현황 분석 문서 추가: - 상세 페이지 62%, 목록 페이지 82% 공통화 달성 - 추가 공통화 기회 및 로드맵 정리 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -24,19 +23,9 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { FileText, Plus, X, Eye, Receipt, List } from 'lucide-react';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { FileText, Plus, X, Eye } from 'lucide-react';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { purchaseConfig } from './purchaseConfig';
|
||||
import { DocumentDetailModal } from '@/components/approval/DocumentDetail';
|
||||
import type { ProposalDocumentData, ExpenseReportDocumentData } from '@/components/approval/DocumentDetail/types';
|
||||
import type { PurchaseRecord, PurchaseItem, PurchaseType } from './types';
|
||||
@@ -73,7 +62,6 @@ const createEmptyItem = (): PurchaseItem => ({
|
||||
});
|
||||
|
||||
export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
const router = useRouter();
|
||||
const isViewMode = mode === 'view';
|
||||
const isNewMode = mode === 'new';
|
||||
|
||||
@@ -100,7 +88,6 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
|
||||
// ===== 다이얼로그 상태 =====
|
||||
const [documentModalOpen, setDocumentModalOpen] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
// ===== 거래처 목록 로드 =====
|
||||
useEffect(() => {
|
||||
@@ -203,11 +190,11 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ===== 저장 =====
|
||||
const handleSave = useCallback(async () => {
|
||||
// ===== 저장 (IntegratedDetailTemplate 호환) =====
|
||||
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
if (!vendorId) {
|
||||
toast.warning('거래처를 선택해주세요.');
|
||||
return;
|
||||
return { success: false, error: '거래처를 선택해주세요.' };
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
@@ -232,94 +219,42 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
|
||||
if (result?.success) {
|
||||
toast.success(isNewMode ? '매입이 등록되었습니다.' : '매입이 수정되었습니다.');
|
||||
router.push('/ko/accounting/purchase');
|
||||
return { success: true };
|
||||
} else {
|
||||
toast.error(result?.error || '저장에 실패했습니다.');
|
||||
return { success: false, error: result?.error || '저장에 실패했습니다.' };
|
||||
}
|
||||
} catch {
|
||||
toast.error('저장 중 오류가 발생했습니다.');
|
||||
return { success: false, error: '저장 중 오류가 발생했습니다.' };
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [purchaseDate, vendorId, totals, purchaseType, taxInvoiceReceived, isNewMode, purchaseId, router]);
|
||||
}, [purchaseDate, vendorId, totals, purchaseType, taxInvoiceReceived, isNewMode, purchaseId]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
router.push('/ko/accounting/purchase');
|
||||
}, [router]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
router.push(`/ko/accounting/purchase/${purchaseId}?mode=edit`);
|
||||
}, [router, purchaseId]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isNewMode) {
|
||||
router.push('/ko/accounting/purchase');
|
||||
} else {
|
||||
router.push(`/ko/accounting/purchase/${purchaseId}`);
|
||||
}
|
||||
}, [router, purchaseId, isNewMode]);
|
||||
|
||||
// ===== 삭제 =====
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!purchaseId) return;
|
||||
// ===== 삭제 (IntegratedDetailTemplate 호환) =====
|
||||
const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
if (!purchaseId) return { success: false, error: 'ID가 없습니다.' };
|
||||
|
||||
try {
|
||||
const result = await deletePurchase(purchaseId);
|
||||
setShowDeleteDialog(false);
|
||||
|
||||
if (result.success) {
|
||||
toast.success('매입이 삭제되었습니다.');
|
||||
router.push('/ko/accounting/purchase');
|
||||
return { success: true };
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
return { success: false, error: result.error || '삭제에 실패했습니다.' };
|
||||
}
|
||||
} catch {
|
||||
toast.error('삭제 중 오류가 발생했습니다.');
|
||||
return { success: false, error: '삭제 중 오류가 발생했습니다.' };
|
||||
}
|
||||
}, [purchaseId, router]);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 페이지 헤더 */}
|
||||
<PageHeader
|
||||
title={isNewMode ? '매입 등록' : '매입 상세'}
|
||||
description="매입 상세를 등록하고 관리합니다"
|
||||
icon={Receipt}
|
||||
/>
|
||||
|
||||
{/* 헤더 액션 버튼 */}
|
||||
<div className="flex items-center justify-end gap-2 mb-6">
|
||||
{/* view 모드: [목록] [삭제] [수정] */}
|
||||
{isViewMode ? (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleBack}>
|
||||
<List className="h-4 w-4 mr-2" />
|
||||
목록
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-red-500 border-red-200 hover:bg-red-50"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
<Button onClick={handleEdit} className="bg-blue-500 hover:bg-blue-600">
|
||||
수정
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
/* edit/new 모드: [취소] [저장/등록] */
|
||||
<>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-blue-500 hover:bg-blue-600" disabled={isSaving}>
|
||||
{isSaving ? '저장 중...' : isNewMode ? '등록' : '저장'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}, [purchaseId]);
|
||||
|
||||
// ===== 폼 내용 렌더링 =====
|
||||
const renderFormContent = () => (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
{/* ===== 기본 정보 섹션 ===== */}
|
||||
<Card>
|
||||
@@ -732,26 +667,33 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ===== 삭제 확인 다이얼로그 ===== */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>매입 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
이 매입 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</PageLayout>
|
||||
</>
|
||||
);
|
||||
|
||||
// ===== 모드 변환 =====
|
||||
const templateMode = isNewMode ? 'create' : mode;
|
||||
|
||||
// ===== 동적 config =====
|
||||
const dynamicConfig = {
|
||||
...purchaseConfig,
|
||||
title: isNewMode ? '매입 등록' : '매입 상세',
|
||||
actions: {
|
||||
...purchaseConfig.actions,
|
||||
submitLabel: isNewMode ? '등록' : '저장',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={dynamicConfig}
|
||||
mode={templateMode}
|
||||
initialData={{}}
|
||||
itemId={purchaseId}
|
||||
isLoading={isLoading}
|
||||
onSubmit={handleSubmit}
|
||||
onDelete={purchaseId && !isNewMode ? handleDelete : undefined}
|
||||
renderView={() => renderFormContent()}
|
||||
renderForm={() => renderFormContent()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Receipt } from 'lucide-react';
|
||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
|
||||
/**
|
||||
* 매입 상세 페이지 Config
|
||||
*
|
||||
* 참고: 이 config는 타이틀/버튼 영역만 정의
|
||||
* 폼 내용은 기존 PurchaseDetail의 renderView/renderForm에서 처리
|
||||
* (품목 테이블, 품의서/지출결의서, 세금계산서 등 특수 기능 유지)
|
||||
*/
|
||||
export const purchaseConfig: DetailConfig = {
|
||||
title: '매입 상세',
|
||||
description: '매입 상세를 등록하고 관리합니다',
|
||||
icon: Receipt,
|
||||
basePath: '/accounting/purchase',
|
||||
fields: [], // renderView/renderForm 사용으로 필드 정의 불필요
|
||||
gridColumns: 2,
|
||||
actions: {
|
||||
showBack: true,
|
||||
showDelete: true,
|
||||
showEdit: true,
|
||||
backLabel: '목록',
|
||||
deleteLabel: '삭제',
|
||||
editLabel: '수정',
|
||||
submitLabel: '저장',
|
||||
cancelLabel: '취소',
|
||||
deleteConfirmMessage: {
|
||||
title: '매입 삭제',
|
||||
description: '이 매입 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.',
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user