- ApprovalBox: 문서 클릭 시 API 데이터 로드하여 모달에 표시 - DocumentCreate: 품의서 폼 개선 및 actions 수정 - 결재자 정보 (직책, 부서) 표시 개선
640 lines
22 KiB
TypeScript
640 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useEffect, useTransition, useRef } from 'react';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { format } from 'date-fns';
|
|
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,
|
|
createApproval,
|
|
createAndSubmitApproval,
|
|
getApprovalById,
|
|
updateApproval,
|
|
updateAndSubmitApproval,
|
|
deleteApproval,
|
|
getEmployees,
|
|
} from './actions';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { Button } from '@/components/ui/button';
|
|
import { BasicInfoSection } from './BasicInfoSection';
|
|
import { ApprovalLineSection } from './ApprovalLineSection';
|
|
import { ReferenceSection } from './ReferenceSection';
|
|
import { ProposalForm } from './ProposalForm';
|
|
import { ExpenseReportForm } from './ExpenseReportForm';
|
|
import { ExpenseEstimateForm } from './ExpenseEstimateForm';
|
|
import { DocumentDetailModal } from '@/components/approval/DocumentDetail';
|
|
import type {
|
|
DocumentType as ModalDocumentType,
|
|
ProposalDocumentData,
|
|
ExpenseReportDocumentData,
|
|
ExpenseEstimateDocumentData,
|
|
} from '@/components/approval/DocumentDetail/types';
|
|
import type {
|
|
BasicInfo,
|
|
ApprovalPerson,
|
|
ProposalData,
|
|
ExpenseReportData,
|
|
ExpenseEstimateData,
|
|
} from './types';
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
import { useDevFill, generatePurchaseApprovalData } from '@/components/dev';
|
|
import { getClients } from '@/components/accounting/VendorManagement/actions';
|
|
|
|
// 초기 데이터 - SSR에서는 빈 문자열, 클라이언트에서 날짜 설정
|
|
const getInitialBasicInfo = (): BasicInfo => ({
|
|
drafter: '홍길동',
|
|
draftDate: '', // 클라이언트에서 설정
|
|
documentNo: '',
|
|
documentType: 'proposal',
|
|
});
|
|
|
|
const getInitialProposalData = (): ProposalData => ({
|
|
vendorId: '',
|
|
vendor: '',
|
|
vendorPaymentDate: '', // 클라이언트에서 설정
|
|
title: '',
|
|
description: '',
|
|
reason: '',
|
|
estimatedCost: 0,
|
|
attachments: [],
|
|
});
|
|
|
|
const getInitialExpenseReportData = (): ExpenseReportData => ({
|
|
requestDate: '', // 클라이언트에서 설정
|
|
paymentDate: '', // 클라이언트에서 설정
|
|
items: [],
|
|
cardId: '',
|
|
totalAmount: 0,
|
|
attachments: [],
|
|
});
|
|
|
|
const getInitialExpenseEstimateData = (): ExpenseEstimateData => ({
|
|
items: [],
|
|
totalExpense: 0,
|
|
accountBalance: 10000000,
|
|
finalDifference: 10000000,
|
|
});
|
|
|
|
export function DocumentCreate() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const [isPending, startTransition] = useTransition();
|
|
const { currentUser } = useAuth();
|
|
|
|
// 수정 모드 / 복제 모드 상태
|
|
const documentId = searchParams.get('id');
|
|
const mode = searchParams.get('mode');
|
|
const copyFromId = searchParams.get('copyFrom');
|
|
const isEditMode = mode === 'edit' && !!documentId;
|
|
const isCopyMode = !!copyFromId;
|
|
const [isLoadingDocument, setIsLoadingDocument] = useState(false);
|
|
|
|
// 상태 관리
|
|
const [basicInfo, setBasicInfo] = useState<BasicInfo>(getInitialBasicInfo);
|
|
const [approvalLine, setApprovalLine] = useState<ApprovalPerson[]>([]);
|
|
const [references, setReferences] = useState<ApprovalPerson[]>([]);
|
|
const [proposalData, setProposalData] = useState<ProposalData>(getInitialProposalData);
|
|
const [expenseReportData, setExpenseReportData] = useState<ExpenseReportData>(getInitialExpenseReportData);
|
|
const [expenseEstimateData, setExpenseEstimateData] = useState<ExpenseEstimateData>(getInitialExpenseEstimateData);
|
|
const [isLoadingEstimate, setIsLoadingEstimate] = useState(false);
|
|
|
|
// 복제 모드 toast 중복 호출 방지
|
|
const copyToastShownRef = useRef(false);
|
|
|
|
// Hydration 불일치 방지: 클라이언트에서만 날짜 초기화
|
|
useEffect(() => {
|
|
const today = format(new Date(), 'yyyy-MM-dd');
|
|
const now = format(new Date(), 'yyyy-MM-dd HH:mm');
|
|
|
|
setBasicInfo(prev => ({ ...prev, draftDate: prev.draftDate || now }));
|
|
setProposalData(prev => ({ ...prev, vendorPaymentDate: prev.vendorPaymentDate || today }));
|
|
setExpenseReportData(prev => ({
|
|
...prev,
|
|
requestDate: prev.requestDate || today,
|
|
paymentDate: prev.paymentDate || today,
|
|
}));
|
|
}, []);
|
|
|
|
// 미리보기 모달 상태
|
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
|
|
// ===== DevFill: 자동 입력 기능 =====
|
|
useDevFill('purchaseApproval', useCallback(async () => {
|
|
if (!isEditMode && !isCopyMode) {
|
|
const mockData = generatePurchaseApprovalData();
|
|
|
|
// 직원 목록 가져오기
|
|
const employees = await getEmployees();
|
|
|
|
// 거래처 목록 가져오기 (매입 거래처만)
|
|
const clientsResult = await getClients({ size: 1000, only_active: true });
|
|
const purchaseClients = clientsResult.success
|
|
? clientsResult.data
|
|
.filter((v) => v.category === 'purchase' || v.category === 'both')
|
|
.map((v) => ({ id: v.id, name: v.vendorName }))
|
|
: [];
|
|
|
|
// 랜덤 거래처 선택
|
|
const randomClient = purchaseClients.length > 0
|
|
? purchaseClients[Math.floor(Math.random() * purchaseClients.length)]
|
|
: null;
|
|
|
|
// localStorage에서 실제 로그인 사용자 이름 가져오기 (우측 상단 표시와 동일한 소스)
|
|
const userDataStr = localStorage.getItem("user");
|
|
const currentUserName = userDataStr ? JSON.parse(userDataStr).name : currentUser?.name;
|
|
|
|
// 현재 사용자 이름으로 결재선에 추가할 직원 찾기
|
|
const approver = currentUserName
|
|
? employees.find(e => e.name === currentUserName)
|
|
: null;
|
|
|
|
// 경리/회계/재무 부서 직원 중 랜덤 1명 참조 추가
|
|
const accountingDepts = ['경리', '회계', '재무'];
|
|
const accountingStaff = employees.filter(e =>
|
|
accountingDepts.some(dept => e.department?.includes(dept))
|
|
);
|
|
const randomReference = accountingStaff.length > 0
|
|
? accountingStaff[Math.floor(Math.random() * accountingStaff.length)]
|
|
: null;
|
|
|
|
setBasicInfo(prev => ({
|
|
...prev,
|
|
...mockData.basicInfo,
|
|
draftDate: prev.draftDate || mockData.basicInfo.draftDate,
|
|
}));
|
|
|
|
// 결재선: 현재 사용자가 직원 목록에 있으면 설정, 없으면 랜덤 1명
|
|
if (approver) {
|
|
setApprovalLine([approver]);
|
|
} else if (employees.length > 0) {
|
|
const randomApprover = employees[Math.floor(Math.random() * employees.length)];
|
|
setApprovalLine([randomApprover]);
|
|
}
|
|
|
|
// 참조: 경리/회계/재무 직원이 있으면 설정
|
|
if (randomReference) {
|
|
setReferences([randomReference]);
|
|
}
|
|
|
|
setProposalData(prev => ({
|
|
...prev,
|
|
...mockData.proposalData,
|
|
// 실제 API 거래처로 덮어쓰기
|
|
vendorId: randomClient?.id || '',
|
|
vendor: randomClient?.name || '',
|
|
}));
|
|
toast.success('지출결의서 데이터가 자동 입력되었습니다.');
|
|
}
|
|
}, [isEditMode, isCopyMode, currentUser?.name]));
|
|
|
|
// 수정 모드: 문서 로드
|
|
useEffect(() => {
|
|
if (!isEditMode || !documentId) return;
|
|
|
|
const loadDocument = async () => {
|
|
setIsLoadingDocument(true);
|
|
try {
|
|
const result = await getApprovalById(parseInt(documentId));
|
|
if (result.success && result.data) {
|
|
const { basicInfo: loadedBasicInfo, approvalLine: loadedApprovalLine, references: loadedReferences, proposalData: loadedProposalData, expenseReportData: loadedExpenseReportData, expenseEstimateData: loadedExpenseEstimateData } = result.data;
|
|
|
|
setBasicInfo(loadedBasicInfo);
|
|
setApprovalLine(loadedApprovalLine);
|
|
setReferences(loadedReferences);
|
|
if (loadedProposalData) setProposalData(loadedProposalData);
|
|
if (loadedExpenseReportData) setExpenseReportData(loadedExpenseReportData);
|
|
if (loadedExpenseEstimateData) setExpenseEstimateData(loadedExpenseEstimateData);
|
|
} else {
|
|
toast.error(result.error || '문서를 불러오는데 실패했습니다.');
|
|
router.back();
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('Failed to load document:', error);
|
|
toast.error('문서를 불러오는데 실패했습니다.');
|
|
router.back();
|
|
} finally {
|
|
setIsLoadingDocument(false);
|
|
}
|
|
};
|
|
|
|
loadDocument();
|
|
}, [isEditMode, documentId, router]);
|
|
|
|
// 복제 모드: 원본 문서 로드 후 새 문서로 설정
|
|
useEffect(() => {
|
|
if (!isCopyMode || !copyFromId) return;
|
|
|
|
const loadDocumentForCopy = async () => {
|
|
setIsLoadingDocument(true);
|
|
try {
|
|
const result = await getApprovalById(parseInt(copyFromId));
|
|
if (result.success && result.data) {
|
|
const { basicInfo: loadedBasicInfo, approvalLine: loadedApprovalLine, references: loadedReferences, proposalData: loadedProposalData, expenseReportData: loadedExpenseReportData, expenseEstimateData: loadedExpenseEstimateData } = result.data;
|
|
|
|
// 복제: 문서번호 초기화, 기안일 현재 시간으로
|
|
const now = format(new Date(), 'yyyy-MM-dd HH:mm');
|
|
setBasicInfo({
|
|
...loadedBasicInfo,
|
|
documentNo: '', // 새 문서이므로 문서번호 초기화
|
|
draftDate: now,
|
|
});
|
|
|
|
// 결재선/참조는 그대로 유지
|
|
setApprovalLine(loadedApprovalLine);
|
|
setReferences(loadedReferences);
|
|
|
|
// 문서 내용 복제
|
|
if (loadedProposalData) setProposalData(loadedProposalData);
|
|
if (loadedExpenseReportData) setExpenseReportData(loadedExpenseReportData);
|
|
if (loadedExpenseEstimateData) setExpenseEstimateData(loadedExpenseEstimateData);
|
|
|
|
// React.StrictMode에서 useEffect 두 번 실행으로 인한 toast 중복 방지
|
|
if (!copyToastShownRef.current) {
|
|
copyToastShownRef.current = true;
|
|
toast.info('문서가 복제되었습니다. 수정 후 상신해주세요.');
|
|
}
|
|
} else {
|
|
toast.error(result.error || '원본 문서를 불러오는데 실패했습니다.');
|
|
router.back();
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('Failed to load document for copy:', error);
|
|
toast.error('원본 문서를 불러오는데 실패했습니다.');
|
|
router.back();
|
|
} finally {
|
|
setIsLoadingDocument(false);
|
|
}
|
|
};
|
|
|
|
loadDocumentForCopy();
|
|
}, [isCopyMode, copyFromId, router]);
|
|
|
|
// 비용견적서 항목 로드
|
|
const loadExpenseEstimateItems = useCallback(async () => {
|
|
setIsLoadingEstimate(true);
|
|
try {
|
|
const result = await getExpenseEstimateItems();
|
|
if (result) {
|
|
setExpenseEstimateData({
|
|
items: result.items,
|
|
totalExpense: result.totalExpense,
|
|
accountBalance: result.accountBalance,
|
|
finalDifference: result.finalDifference,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('Failed to load expense estimate items:', error);
|
|
toast.error('비용견적서 항목을 불러오는데 실패했습니다.');
|
|
} finally {
|
|
setIsLoadingEstimate(false);
|
|
}
|
|
}, []);
|
|
|
|
// 문서 유형이 비용견적서로 변경되면 항목 로드
|
|
useEffect(() => {
|
|
if (basicInfo.documentType === 'expenseEstimate' && expenseEstimateData.items.length === 0) {
|
|
loadExpenseEstimateItems();
|
|
}
|
|
}, [basicInfo.documentType, expenseEstimateData.items.length, loadExpenseEstimateItems]);
|
|
|
|
// 폼 데이터 수집
|
|
const getFormData = useCallback(() => {
|
|
return {
|
|
basicInfo,
|
|
approvalLine,
|
|
references,
|
|
proposalData: basicInfo.documentType === 'proposal' ? proposalData : undefined,
|
|
expenseReportData: basicInfo.documentType === 'expenseReport' ? expenseReportData : undefined,
|
|
expenseEstimateData: basicInfo.documentType === 'expenseEstimate' ? expenseEstimateData : undefined,
|
|
};
|
|
}, [basicInfo, approvalLine, references, proposalData, expenseReportData, expenseEstimateData]);
|
|
|
|
// 핸들러
|
|
const handleBack = useCallback(() => {
|
|
router.back();
|
|
}, [router]);
|
|
|
|
const handleDelete = useCallback(async () => {
|
|
if (!confirm('작성 중인 문서를 삭제하시겠습니까?')) {
|
|
return;
|
|
}
|
|
|
|
// 수정 모드: 실제 문서 삭제
|
|
if (isEditMode && documentId) {
|
|
startTransition(async () => {
|
|
try {
|
|
const result = await deleteApproval(parseInt(documentId));
|
|
if (result.success) {
|
|
toast.success('문서가 삭제되었습니다.');
|
|
router.back();
|
|
} else {
|
|
toast.error(result.error || '문서 삭제에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('Delete error:', error);
|
|
toast.error('문서 삭제 중 오류가 발생했습니다.');
|
|
}
|
|
});
|
|
} else {
|
|
// 새 문서: 그냥 뒤로가기
|
|
router.back();
|
|
}
|
|
}, [router, isEditMode, documentId]);
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
// 유효성 검사
|
|
if (approvalLine.length === 0) {
|
|
toast.error('결재선을 지정해주세요.');
|
|
return;
|
|
}
|
|
|
|
startTransition(async () => {
|
|
try {
|
|
const formData = getFormData();
|
|
|
|
// 수정 모드: 수정 후 상신
|
|
if (isEditMode && documentId) {
|
|
const result = await updateAndSubmitApproval(parseInt(documentId), formData);
|
|
if (result.success) {
|
|
toast.success('수정 및 상신 완료', {
|
|
description: `문서번호: ${result.data?.documentNo}`,
|
|
});
|
|
router.back();
|
|
} else {
|
|
toast.error(result.error || '문서 상신에 실패했습니다.');
|
|
}
|
|
} else {
|
|
// 새 문서: 생성 후 상신
|
|
const result = await createAndSubmitApproval(formData);
|
|
if (result.success) {
|
|
toast.success('상신 완료', {
|
|
description: `문서번호: ${result.data?.documentNo}`,
|
|
});
|
|
router.back();
|
|
} else {
|
|
toast.error(result.error || '문서 상신에 실패했습니다.');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('Submit error:', error);
|
|
toast.error('문서 상신 중 오류가 발생했습니다.');
|
|
}
|
|
});
|
|
}, [approvalLine, getFormData, router, isEditMode, documentId]);
|
|
|
|
const handleSaveDraft = useCallback(async () => {
|
|
startTransition(async () => {
|
|
try {
|
|
const formData = getFormData();
|
|
|
|
// 수정 모드: 기존 문서 업데이트
|
|
if (isEditMode && documentId) {
|
|
const result = await updateApproval(parseInt(documentId), formData);
|
|
if (result.success) {
|
|
toast.success('저장 완료', {
|
|
description: `문서번호: ${result.data?.documentNo}`,
|
|
});
|
|
} else {
|
|
toast.error(result.error || '저장에 실패했습니다.');
|
|
}
|
|
} else {
|
|
// 새 문서: 임시저장
|
|
const result = await createApproval(formData);
|
|
if (result.success) {
|
|
toast.success('임시저장 완료', {
|
|
description: `문서번호: ${result.data?.documentNo}`,
|
|
});
|
|
// 문서번호 업데이트
|
|
if (result.data?.documentNo) {
|
|
setBasicInfo(prev => ({ ...prev, documentNo: result.data!.documentNo }));
|
|
}
|
|
} else {
|
|
toast.error(result.error || '임시저장에 실패했습니다.');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('Save draft error:', error);
|
|
toast.error('저장 중 오류가 발생했습니다.');
|
|
}
|
|
});
|
|
}, [getFormData, isEditMode, documentId]);
|
|
|
|
// 미리보기 핸들러
|
|
const handlePreview = useCallback(() => {
|
|
setIsPreviewOpen(true);
|
|
}, []);
|
|
|
|
// 미리보기용 데이터 변환
|
|
const getPreviewData = useCallback((): ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData => {
|
|
const drafter = {
|
|
id: 'drafter-1',
|
|
name: basicInfo.drafter,
|
|
position: '사원',
|
|
department: '개발팀',
|
|
status: 'approved' as const,
|
|
};
|
|
const approvers = approvalLine.map((a, index) => ({
|
|
id: a.id,
|
|
name: a.name,
|
|
position: a.position,
|
|
department: a.department,
|
|
status: (index === 0 ? 'pending' : 'none') as 'pending' | 'approved' | 'rejected' | 'none',
|
|
}));
|
|
|
|
switch (basicInfo.documentType) {
|
|
case 'expenseEstimate':
|
|
return {
|
|
documentNo: basicInfo.documentNo || '미발급',
|
|
createdAt: basicInfo.draftDate,
|
|
items: expenseEstimateData.items.map(item => ({
|
|
id: item.id,
|
|
expectedPaymentDate: item.expectedPaymentDate,
|
|
category: item.category,
|
|
amount: item.amount,
|
|
vendor: item.vendor,
|
|
account: item.memo || '',
|
|
})),
|
|
totalExpense: expenseEstimateData.totalExpense,
|
|
accountBalance: expenseEstimateData.accountBalance,
|
|
finalDifference: expenseEstimateData.finalDifference,
|
|
approvers,
|
|
drafter,
|
|
};
|
|
case 'expenseReport':
|
|
return {
|
|
documentNo: basicInfo.documentNo || '미발급',
|
|
createdAt: basicInfo.draftDate,
|
|
requestDate: expenseReportData.requestDate,
|
|
paymentDate: expenseReportData.paymentDate,
|
|
items: expenseReportData.items.map((item, index) => ({
|
|
id: item.id,
|
|
no: index + 1,
|
|
description: item.description,
|
|
amount: item.amount,
|
|
note: item.note,
|
|
})),
|
|
cardInfo: expenseReportData.cardId || '-',
|
|
totalAmount: expenseReportData.totalAmount,
|
|
attachments: expenseReportData.attachments.map(f => f.name),
|
|
approvers,
|
|
drafter,
|
|
};
|
|
default: {
|
|
// 이미 업로드된 파일 URL (Next.js 프록시 사용) + 새로 추가된 파일 미리보기 URL
|
|
const uploadedFileUrls = (proposalData.uploadedFiles || []).map(f =>
|
|
`/api/proxy/files/${f.id}/download`
|
|
);
|
|
const newFileUrls = proposalData.attachments.map(f => URL.createObjectURL(f));
|
|
|
|
return {
|
|
documentNo: basicInfo.documentNo || '미발급',
|
|
createdAt: basicInfo.draftDate,
|
|
vendor: proposalData.vendor || '-',
|
|
vendorPaymentDate: proposalData.vendorPaymentDate,
|
|
title: proposalData.title || '(제목 없음)',
|
|
description: proposalData.description || '-',
|
|
reason: proposalData.reason || '-',
|
|
estimatedCost: proposalData.estimatedCost,
|
|
attachments: [...uploadedFileUrls, ...newFileUrls],
|
|
approvers,
|
|
drafter,
|
|
};
|
|
}
|
|
}
|
|
}, [basicInfo, approvalLine, proposalData, expenseReportData, expenseEstimateData]);
|
|
|
|
// 문서 유형별 폼 렌더링
|
|
const renderDocumentTypeForm = () => {
|
|
switch (basicInfo.documentType) {
|
|
case 'proposal':
|
|
return <ProposalForm data={proposalData} onChange={setProposalData} />;
|
|
case 'expenseReport':
|
|
return <ExpenseReportForm data={expenseReportData} onChange={setExpenseReportData} />;
|
|
case 'expenseEstimate':
|
|
return <ExpenseEstimateForm data={expenseEstimateData} onChange={setExpenseEstimateData} isLoading={isLoadingEstimate} />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// 현재 모드에 맞는 config 선택
|
|
const currentConfig = isEditMode
|
|
? documentEditConfig
|
|
: isCopyMode
|
|
? documentCopyConfig
|
|
: documentCreateConfig;
|
|
|
|
// 헤더 액션 버튼 렌더링
|
|
const renderHeaderActions = useCallback(() => {
|
|
return (
|
|
<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"
|
|
size="sm"
|
|
onClick={handleDelete}
|
|
disabled={isPending}
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-1" />
|
|
삭제
|
|
</Button>
|
|
<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"
|
|
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} />
|
|
|
|
{/* 결재선 */}
|
|
<ApprovalLineSection data={approvalLine} onChange={setApprovalLine} />
|
|
|
|
{/* 참조 */}
|
|
<ReferenceSection data={references} onChange={setReferences} />
|
|
|
|
{/* 문서 유형별 폼 */}
|
|
{renderDocumentTypeForm()}
|
|
</div>
|
|
);
|
|
}, [basicInfo, approvalLine, references, renderDocumentTypeForm]);
|
|
|
|
return (
|
|
<>
|
|
<IntegratedDetailTemplate
|
|
config={currentConfig}
|
|
mode={isEditMode ? 'edit' : 'create'}
|
|
isLoading={isLoadingDocument}
|
|
onBack={handleBack}
|
|
renderForm={renderFormContent}
|
|
headerActions={renderHeaderActions()}
|
|
/>
|
|
|
|
{/* 미리보기 모달 */}
|
|
<DocumentDetailModal
|
|
open={isPreviewOpen}
|
|
onOpenChange={setIsPreviewOpen}
|
|
documentType={basicInfo.documentType as ModalDocumentType}
|
|
data={getPreviewData()}
|
|
mode="draft"
|
|
documentStatus="draft"
|
|
onCopy={() => {
|
|
// 복제: 현재 데이터를 기반으로 새 문서 등록 화면으로 이동
|
|
if (documentId) {
|
|
router.push(`/ko/approval/draft/new?copyFrom=${documentId}`);
|
|
setIsPreviewOpen(false);
|
|
}
|
|
}}
|
|
onSubmit={() => {
|
|
setIsPreviewOpen(false);
|
|
handleSubmit();
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|