feat(WEB): IntegratedDetailTemplate 통합 템플릿 구현 및 Phase 1~8 마이그레이션

- Phase 1: 기안함(DocumentCreate) 마이그레이션
- Phase 2: 작업지시(WorkOrderCreate/Edit) 마이그레이션
- Phase 3: 출하(ShipmentCreate/Edit) 마이그레이션
- Phase 4: 사원(EmployeeForm) 마이그레이션
- Phase 5: 게시판(BoardForm) 마이그레이션
- Phase 6: 1:1문의(InquiryForm) 마이그레이션
- Phase 7: 공정(ProcessForm) 마이그레이션
- Phase 8: 수입검사/품질검사(InspectionCreate) 마이그레이션
- DetailActions에 showSave 옵션 추가
- 각 도메인별 config 파일 생성

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-20 19:31:07 +09:00
parent 6b0ffc810b
commit 62ef2b1ff9
24 changed files with 861 additions and 534 deletions

View File

@@ -0,0 +1,34 @@
/**
* 기안 문서 작성/수정 페이지 설정
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
*/
import { FileText } from 'lucide-react';
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
export const documentCreateConfig: DetailConfig = {
title: '문서 작성',
description: '새로운 결재 문서를 작성합니다',
icon: FileText,
basePath: '/approval/draft',
fields: [],
actions: {
showBack: true,
showEdit: false,
showDelete: false, // 커스텀 삭제 버튼 사용
showSave: false, // 상신/임시저장 버튼 사용
},
};
export const documentEditConfig: DetailConfig = {
...documentCreateConfig,
title: '문서 수정',
description: '기존 결재 문서를 수정합니다',
// actions는 documentCreateConfig에서 상속 (커스텀 버튼 사용)
};
export const documentCopyConfig: DetailConfig = {
...documentCreateConfig,
title: '문서 복제',
description: '복제된 문서를 수정 후 상신합니다',
};

View File

@@ -3,13 +3,16 @@
import { useState, useCallback, useEffect, useTransition, useRef } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { format } from 'date-fns';
import { FileText, Trash2, Send, Save, ArrowLeft, Eye } from 'lucide-react';
import { Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Trash2, Send, Save, Eye, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import {
documentCreateConfig,
documentEditConfig,
documentCopyConfig,
} from './documentCreateConfig';
import {
getExpenseEstimateItems,
getEmployees,
createApproval,
createAndSubmitApproval,
getApprovalById,
@@ -18,7 +21,6 @@ import {
deleteApproval,
} from './actions';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { BasicInfoSection } from './BasicInfoSection';
import { ApprovalLineSection } from './ApprovalLineSection';
import { ReferenceSection } from './ReferenceSection';
@@ -33,7 +35,6 @@ import type {
ExpenseEstimateDocumentData,
} from '@/components/approval/DocumentDetail/types';
import type {
DocumentType,
BasicInfo,
ApprovalPerson,
ProposalData,
@@ -416,7 +417,7 @@ export function DocumentCreate() {
approvers,
drafter,
};
default:
default: {
// 이미 업로드된 파일 URL (Next.js 프록시 사용) + 새로 추가된 파일 미리보기 URL
const uploadedFileUrls = (proposalData.uploadedFiles || []).map(f =>
`/api/proxy/files/${f.id}/download`
@@ -436,6 +437,7 @@ export function DocumentCreate() {
approvers,
drafter,
};
}
}
}, [basicInfo, approvalLine, proposalData, expenseReportData, expenseEstimateData]);
@@ -453,75 +455,63 @@ export function DocumentCreate() {
}
};
// 문서 로딩 중
if (isLoadingDocument) {
// 현재 모드에 맞는 config 선택
const currentConfig = isEditMode
? documentEditConfig
: isCopyMode
? documentCopyConfig
: documentCreateConfig;
// 헤더 액션 버튼 렌더링
const renderHeaderActions = useCallback(() => {
return (
<div className="container mx-auto py-6 px-4 max-w-4xl">
<Card className="mb-6">
<CardHeader>
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={handleBack}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-2">
<FileText className="h-6 w-6 text-primary" />
<div>
<CardTitle> ...</CardTitle>
</div>
</div>
</div>
</CardHeader>
</Card>
<ContentLoadingSpinner text="문서를 불러오는 중..." />
</div>
);
}
return (
<div className="container mx-auto py-6 px-4 max-w-4xl">
{/* 헤더 */}
<Card className="mb-6">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={handleBack}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex items-center gap-2">
<FileText className="h-6 w-6 text-primary" />
<div>
<CardTitle>{isEditMode ? '문서 수정' : isCopyMode ? '문서 복제' : '문서 작성'}</CardTitle>
<CardDescription>
{isEditMode ? '기존 문서를 수정합니다' : isCopyMode ? '복제된 문서를 수정 후 상신합니다' : '새로운 문서를 작성합니다'}
</CardDescription>
</div>
</div>
</div>
</div>
</CardHeader>
</Card>
{/* 액션 버튼 (스텝) */}
<div className="flex items-center justify-center gap-2 mb-6">
<Button variant="outline" className="min-w-[80px]" onClick={handlePreview}>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handlePreview}>
<Eye className="w-4 h-4 mr-1" />
</Button>
<Button variant="outline" className="min-w-[80px]" onClick={handleDelete} disabled={isPending}>
<Button
variant="outline"
size="sm"
onClick={handleDelete}
disabled={isPending}
>
<Trash2 className="w-4 h-4 mr-1" />
</Button>
<Button variant="default" className="min-w-[80px]" onClick={handleSubmit} disabled={isPending}>
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Send className="w-4 h-4 mr-1" />}
<Button
variant="default"
size="sm"
onClick={handleSubmit}
disabled={isPending}
>
{isPending ? (
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
) : (
<Send className="w-4 h-4 mr-1" />
)}
</Button>
<Button variant="secondary" className="min-w-[80px]" onClick={handleSaveDraft} disabled={isPending}>
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Save className="w-4 h-4 mr-1" />}
<Button
variant="secondary"
size="sm"
onClick={handleSaveDraft}
disabled={isPending}
>
{isPending ? (
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
) : (
<Save className="w-4 h-4 mr-1" />
)}
{isEditMode ? '저장' : '임시저장'}
</Button>
</div>
);
}, [handlePreview, handleDelete, handleSubmit, handleSaveDraft, isPending, isEditMode]);
{/* 폼 영역 */}
// 폼 컨텐츠 렌더링
const renderFormContent = useCallback(() => {
return (
<div className="space-y-6">
{/* 기본 정보 */}
<BasicInfoSection data={basicInfo} onChange={setBasicInfo} />
@@ -535,27 +525,19 @@ export function DocumentCreate() {
{/* 문서 유형별 폼 */}
{renderDocumentTypeForm()}
</div>
);
}, [basicInfo, approvalLine, references, renderDocumentTypeForm]);
{/* 하단 고정 버튼 (모바일) */}
<div className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t md:hidden">
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={handleDelete} disabled={isPending}>
<Trash2 className="w-4 h-4 mr-1" />
</Button>
<Button variant="secondary" className="flex-1" onClick={handleSaveDraft} disabled={isPending}>
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Save className="w-4 h-4 mr-1" />}
{isEditMode ? '저장' : '임시저장'}
</Button>
<Button variant="default" className="flex-1" onClick={handleSubmit} disabled={isPending}>
{isPending ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Send className="w-4 h-4 mr-1" />}
</Button>
</div>
</div>
{/* 모바일 하단 여백 */}
<div className="h-20 md:hidden" />
return (
<>
<IntegratedDetailTemplate
config={currentConfig}
mode={isEditMode ? 'edit' : 'create'}
isLoading={isLoadingDocument}
onBack={handleBack}
renderForm={renderFormContent}
headerActions={renderHeaderActions()}
/>
{/* 미리보기 모달 */}
<DocumentDetailModal
@@ -577,6 +559,6 @@ export function DocumentCreate() {
handleSubmit();
}}
/>
</div>
</>
);
}