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:
유병철
2026-01-20 15:51:02 +09:00
parent 6f457b28f3
commit 61e3a0ed60
71 changed files with 4743 additions and 4402 deletions

View File

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

View File

@@ -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: '이 매입 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.',
},
},
};