"use client"; import * as React from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { formatDate } from "@/lib/utils"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import type { WebViewerInstance } from "@pdftron/webviewer"; import type { BasicContractView } from "@/db/schema"; import { Upload, FileSignature, CheckCircle2, Search, Clock, FileText, User, AlertCircle, Calendar, Loader2, ArrowRight, Trophy, Target, Shield } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { useRouter } from "next/navigation" import { BasicContractSignViewer } from "../viewer/basic-contract-sign-viewer"; import { getVendorAttachments, processBuyerSignatureAction } from "../service"; // 계약서 상태 타입 정의 interface ContractStatus { id: number; status: 'pending' | 'completed' | 'error'; errorMessage?: string; } interface BasicContractSignDialogProps { contracts: BasicContractView[]; onSuccess?: () => void; hasSelectedRows?: boolean; mode?: 'vendor' | 'buyer'; onBuyerSignComplete?: (contractId: number, signedData: ArrayBuffer) => void; t: (key: string) => string; // 외부 상태 제어를 위한 새로운 props (선택적) open?: boolean; onOpenChange?: (open: boolean) => void; } export function BasicContractSignDialog({ contracts, onSuccess, hasSelectedRows = false, mode = 'vendor', onBuyerSignComplete, t, // 새로 추가된 props open: externalOpen, onOpenChange: externalOnOpenChange }: BasicContractSignDialogProps) { // 내부 상태 (외부 제어가 없을 때 사용) const [internalOpen, setInternalOpen] = React.useState(false); const [selectedContract, setSelectedContract] = React.useState(null); const [instance, setInstance] = React.useState(null); const [searchTerm, setSearchTerm] = React.useState(""); const [isSubmitting, setIsSubmitting] = React.useState(false); // 추가된 state들 const [additionalFiles, setAdditionalFiles] = React.useState([]); const [isLoadingAttachments, setIsLoadingAttachments] = React.useState(false); // 계약서 상태 관리 const [contractStatuses, setContractStatuses] = React.useState([]); // 서명/설문/GTC 코멘트 완료 상태 관리 const [surveyCompletionStatus, setSurveyCompletionStatus] = React.useState>({}); const [signatureStatus, setSignatureStatus] = React.useState>({}); const [gtcCommentStatus, setGtcCommentStatus] = React.useState>({}); console.log(gtcCommentStatus, "gtcCommentStatus") const router = useRouter() // 실제 사용할 open 상태 (외부 제어가 있으면 외부 상태 사용, 없으면 내부 상태 사용) const isControlledExternally = externalOpen !== undefined; const open = isControlledExternally ? externalOpen : internalOpen; // 모드에 따른 텍스트 const isBuyerMode = mode === 'buyer'; const dialogTitle = isBuyerMode ? "구매자 최종승인 서명" : t("basicContracts.dialog.title"); const signButtonText = isBuyerMode ? "최종승인 완료" : "서명 완료 및 저장"; // 버튼 비활성화 조건 const isButtonDisabled = !hasSelectedRows || contracts.length === 0; // 비활성화 이유 텍스트 const getDisabledReason = () => { if (!hasSelectedRows) { return t("basicContracts.toolbar.selectRows"); } if (contracts.length === 0) { return t("basicContracts.toolbar.noPendingContracts"); } return ""; }; // 현재 선택된 계약서의 서명 완료 가능 여부 확인 const canCompleteCurrentContract = React.useMemo(() => { if (!selectedContract) return false; const contractId = selectedContract.id; if (isBuyerMode) { const signatureCompleted = signatureStatus[contractId] === true; return signatureCompleted; } const isComplianceTemplate = selectedContract.templateName?.includes('준법'); const isGTCTemplate = selectedContract.templateName?.includes('GTC'); const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true; // GTC 체크 수정 const gtcStatus = gtcCommentStatus[contractId]; const gtcCompleted = isGTCTemplate ? (!gtcStatus?.hasComments || gtcStatus?.isComplete === true) : true; const signatureCompleted = signatureStatus[contractId] === true; return surveyCompleted && gtcCompleted && signatureCompleted; }, [selectedContract, surveyCompletionStatus, signatureStatus, gtcCommentStatus, isBuyerMode]); // 계약서별 상태 초기화 React.useEffect(() => { if (contracts.length > 0 && contractStatuses.length === 0) { setContractStatuses( contracts.map(contract => ({ id: contract.id, status: 'pending' as const })) ); } }, [contracts, contractStatuses.length]); // 완료된 계약서 수 계산 const completedCount = contractStatuses.filter(status => status.status === 'completed').length; const totalCount = contracts.length; const allCompleted = completedCount === totalCount && totalCount > 0; // 현재 선택된 계약서의 상태 const currentContractStatus = selectedContract ? contractStatuses.find(status => status.id === selectedContract.id) : null; // 다음 미완료 계약서 찾기 const getNextPendingContract = () => { const pendingStatuses = contractStatuses.filter(status => status.status === 'pending'); if (pendingStatuses.length === 0) return null; const nextPendingId = pendingStatuses[0].id; return contracts.find(contract => contract.id === nextPendingId) || null; }; // 다이얼로그 열기/닫기 핸들러 (외부 제어 지원) const handleOpenChange = (isOpen: boolean) => { if (!isOpen && !allCompleted && completedCount > 0) { // 완료되지 않은 계약서가 있으면 확인 대화상자 const confirmClose = window.confirm( `${completedCount}/${totalCount}개 계약서가 완료되었습니다. 정말 나가시겠습니까?` ); if (!confirmClose) return; } // 외부 제어가 있으면 외부 콜백 호출, 없으면 내부 상태 업데이트 if (isControlledExternally && externalOnOpenChange) { externalOnOpenChange(isOpen); } else { setInternalOpen(isOpen); } if (!isOpen) { // 다이얼로그 닫을 때 상태 초기화 setSelectedContract(null); setSearchTerm(""); setAdditionalFiles([]); setContractStatuses([]); setSurveyCompletionStatus({}); setSignatureStatus({}); setGtcCommentStatus({}); // WebViewer 인스턴스 정리 if (instance) { try { instance.UI.dispose(); } catch (error) { console.log("WebViewer dispose error:", error); } setInstance(null); } } }; // 계약서 선택 핸들러 const handleSelectContract = (contract: BasicContractView) => { console.log("계약서 선택:", contract.id, contract.templateName); setSelectedContract(contract); }; // 검색된 계약서 필터링 const filteredContracts = React.useMemo(() => { if (!searchTerm.trim()) return contracts; const term = searchTerm.toLowerCase(); return contracts.filter(contract => (contract.templateName || '').toLowerCase().includes(term) || (contract.requestedByName || '').toLowerCase().includes(term) ); }, [contracts, searchTerm]); // 다이얼로그가 열릴 때 첫 번째 미완료 계약서 자동 선택 React.useEffect(() => { if (open && contracts.length > 0 && !selectedContract) { const firstPending = getNextPendingContract(); if (firstPending) { setSelectedContract(firstPending); } else { setSelectedContract(contracts[0]); } } }, [open, contracts, selectedContract, contractStatuses]); // 추가 파일 가져오기 useEffect (구매자 모드에서는 스킵) React.useEffect(() => { if (isBuyerMode) { setAdditionalFiles([]); return; } const fetchAdditionalFiles = async () => { if (!selectedContract) { setAdditionalFiles([]); return; } // "비밀유지 계약서"인 경우에만 추가 파일 가져오기 if (selectedContract.templateName === "비밀유지 계약서") { setIsLoadingAttachments(true); try { const result = await getVendorAttachments(selectedContract.vendorId); if (result.success) { setAdditionalFiles(result.data); console.log("추가 파일 로드됨:", result.data); } else { console.error("Failed to fetch attachments:", result.error); setAdditionalFiles([]); } } catch (error) { console.error("Error fetching attachments:", error); setAdditionalFiles([]); } finally { setIsLoadingAttachments(false); } } else { setAdditionalFiles([]); } }; fetchAdditionalFiles(); }, [selectedContract, isBuyerMode]); // 설문조사 완료 콜백 함수 const handleSurveyComplete = React.useCallback((contractId: number) => { console.log(`📋 설문조사 완료: 계약서 ${contractId}`); setSurveyCompletionStatus(prev => ({ ...prev, [contractId]: true })); }, []); // 서명 완료 콜백 함수 const handleSignatureComplete = React.useCallback((contractId: number) => { console.log(`✍️ 서명 완료: 계약서 ${contractId}`); setSignatureStatus(prev => ({ ...prev, [contractId]: true })); }, []); // GTC 코멘트 상태 변경 콜백 함수 const handleGtcCommentStatusChange = React.useCallback(( contractId: number, hasComments: boolean, commentCount: number, reviewStatus?: string, isComplete?: boolean ) => { console.log(`📋 GTC 상태 변경: 계약서 ${contractId}, 코멘트 ${commentCount}개, 상태: ${reviewStatus}, 완료: ${isComplete}`); setGtcCommentStatus(prev => ({ ...prev, [contractId]: { hasComments, commentCount, reviewStatus, isComplete } })); }, []); // 서명 완료 핸들러 const completeSign = async () => { if (!instance || !selectedContract) return; // 서명 완료 가능 여부 재확인 if (!canCompleteCurrentContract) { const contractId = selectedContract.id; if (isBuyerMode) { const signatureCompleted = signatureStatus[contractId] === true; if (!signatureCompleted) { toast.error("계약서에 서명을 먼저 완료해주세요.", { description: "문서의 서명 필드에 서명해주세요.", icon: }); return; } } else { // 협력업체 모드의 기존 검증 로직 const isComplianceTemplate = selectedContract.templateName?.includes('준법'); const isGTCTemplate = selectedContract.templateName?.includes('GTC'); const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true; const gtcCompleted = isGTCTemplate ? (gtcCommentStatus[contractId]?.isComplete !== true) : true; const signatureCompleted = signatureStatus[contractId] === true; if (!surveyCompleted) { toast.error("준법 설문조사를 먼저 완료해주세요.", { description: "설문조사 탭에서 모든 필수 항목을 완료해주세요.", icon: }); return; } if (!gtcCompleted) { toast.error("GTC 조항에 코멘트가 있어 서명할 수 없습니다.", { description: "조항 검토 탭에서 모든 코멘트를 삭제하거나 협의를 완료해주세요.", icon: }); return; } if (!signatureCompleted) { toast.error("계약서에 서명을 먼저 완료해주세요.", { description: "문서의 서명 필드에 서명해주세요.", icon: }); return; } } return; } setIsSubmitting(true); try { const { documentViewer, annotationManager } = instance.Core; const doc = documentViewer.getDocument(); const xfdfString = await annotationManager.exportAnnotations(); // 폼 필드 데이터 수집 const fieldManager = annotationManager.getFieldManager(); const fields = fieldManager.getFields(); const formData: any = {}; fields.forEach((field: any) => { formData[field.name] = field.value; }); const data = await doc.getFileData({ xfdfString, downloadType: "pdf", }); if (isBuyerMode) { // 구매자 모드: 최종승인 처리 const result = await processBuyerSignatureAction( selectedContract.id, data, selectedContract.signedFileName || `contract_${selectedContract.id}_buyer_signed.pdf` ); if (result.success) { // 성공시 해당 계약서 상태를 완료로 업데이트 setContractStatuses(prev => prev.map(status => status.id === selectedContract.id ? { ...status, status: 'completed' as const } : status ) ); toast.success("최종승인이 완료되었습니다!", { description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`, icon: }); // 구매자 서명 완료 콜백 호출 if (onBuyerSignComplete) { onBuyerSignComplete(selectedContract.id, data); } // 다음 미완료 계약서로 자동 이동 const nextContract = getNextPendingContract(); if (nextContract) { setSelectedContract(nextContract); toast.info(`다음 계약서로 이동합니다`, { description: nextContract.templateName, icon: }); } else { // 모든 계약서 완료시 toast.success("🎉 모든 계약서 최종승인이 완료되었습니다!", { description: `총 ${totalCount}개 계약서 승인 완료`, icon: }); } router.refresh(); } else { // 실패시 에러 상태 업데이트 setContractStatuses(prev => prev.map(status => status.id === selectedContract.id ? { ...status, status: 'error' as const, errorMessage: result.message } : status ) ); toast.error("최종승인 처리 중 오류가 발생했습니다", { description: result.message, icon: }); } } else { // 협력업체 모드: 기존 로직 const submitFormData = new FormData(); submitFormData.append('file', new Blob([data], { type: 'application/pdf' })); submitFormData.append('tableRowId', selectedContract.id.toString()); submitFormData.append('templateName', selectedContract.signedFileName || ''); // 폼 필드 데이터 추가 if (Object.keys(formData).length > 0) { submitFormData.append('formData', JSON.stringify(formData)); } // API 호출 const response = await fetch('/api/upload/signed-contract', { method: 'POST', body: submitFormData, next: { tags: ["basicContractView-vendor"] }, }); const result = await response.json(); if (result.result) { // 성공시 해당 계약서 상태를 완료로 업데이트 setContractStatuses(prev => prev.map(status => status.id === selectedContract.id ? { ...status, status: 'completed' as const } : status ) ); toast.success("계약서 서명이 완료되었습니다!", { description: `${selectedContract.templateName} - ${completedCount + 1}/${totalCount}개 완료`, icon: }); // 다음 미완료 계약서로 자동 이동 const nextContract = getNextPendingContract(); if (nextContract) { setSelectedContract(nextContract); toast.info(`다음 계약서로 이동합니다`, { description: nextContract.templateName, icon: }); } else { // 모든 계약서 완료시 toast.success("🎉 모든 계약서 서명이 완료되었습니다!", { description: `총 ${totalCount}개 계약서 서명 완료`, icon: }); } router.refresh(); } else { // 실패시 에러 상태 업데이트 setContractStatuses(prev => prev.map(status => status.id === selectedContract.id ? { ...status, status: 'error' as const, errorMessage: result.error } : status ) ); toast.error("서명 처리 중 오류가 발생했습니다", { description: result.error, icon: }); } } } catch (error) { console.error("서명 완료 중 오류:", error); // 에러 상태 업데이트 setContractStatuses(prev => prev.map(status => status.id === selectedContract.id ? { ...status, status: 'error' as const, errorMessage: '서명 처리 중 오류가 발생했습니다' } : status ) ); toast.error("서명 처리 중 오류가 발생했습니다"); } finally { setIsSubmitting(false); } }; // 모든 서명 완료 핸들러 const completeAllSigns = () => { handleOpenChange(false); if (onSuccess) { onSuccess(); } const successMessage = isBuyerMode ? "모든 계약서 최종승인이 완료되었습니다!" : "모든 계약서 서명이 완료되었습니다!"; toast.success(successMessage, { description: "계약서 관리 페이지가 새고침됩니다.", icon: }); }; return ( <> {/* 서명 버튼 - 외부 제어가 없을 때만 표시 */} {!isControlledExternally && ( )} {/* 서명 다이얼로그 */} {/* 고정 헤더 - 진행 상황 표시 */}
{isBuyerMode ? ( ) : ( )} {dialogTitle} {/* 진행 상황 표시 */} {completedCount}/{totalCount} 완료 {/* 추가 파일 로딩 표시 */} {isLoadingAttachments && ( )}
{allCompleted && ( 전체 완료! )}
{/* 진행률 바 */} {totalCount > 1 && (
전체 진행률 {Math.round((completedCount / totalCount) * 100)}%
)} {/* 메인 컨텐츠 영역 - Flexbox 사용 */}
{/* 왼쪽 영역 - 계약서 목록 (고정 너비) */}
setSearchTerm(e.target.value)} />
{filteredContracts.length === 0 ? (

{t("basicContracts.dialog.noDocuments")}

) : (
{filteredContracts.map((contract) => { const contractStatus = contractStatuses.find(status => status.id === contract.id); const isCompleted = contractStatus?.status === 'completed'; const hasError = contractStatus?.status === 'error'; // 계약서별 완료 상태 확인 const isComplianceTemplate = contract.templateName?.includes('준법'); const isGTCTemplate = contract.templateName?.includes('GTC'); const hasSurveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contract.id] === true : true; const hasGtcCompleted = isGTCTemplate ? (gtcCommentStatus[contract.id]?.hasComments !== true) : true; const hasSignatureCompleted = signatureStatus[contract.id] === true; return ( ); })}
)}
{/* 오른쪽 영역 - 문서 뷰어 (확장 가능) */}
{selectedContract ? ( <> {/* 뷰어 헤더 */}

{isBuyerMode ? ( ) : ( )} {selectedContract.templateName || t("basicContracts.dialog.document")} {/* 현재 계약서 상태 표시 */} {currentContractStatus?.status === 'completed' ? ( {isBuyerMode ? "승인 완료" : "서명 완료"} ) : currentContractStatus?.status === 'error' ? ( 처리 실패 ) : ( {isBuyerMode ? "승인 대기" : "서명 대기"} )} {/* 구매자 모드 배지 */} {isBuyerMode && ( 구매자 모드 )} {/* 준법/GTC 템플릿 표시 (구매자 모드가 아닐 때만) */} {!isBuyerMode && selectedContract.templateName?.includes('준법') && ( 준법 서류 )} {!isBuyerMode && selectedContract.templateName?.includes('GTC') && ( GTC 계약서 )} {/* 비밀유지 계약서인 경우 추가 파일 수 표시 (구매자 모드가 아닐 때만) */} {!isBuyerMode && selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && ( 첨부파일 {additionalFiles.length}개 )}

{t("basicContracts.dialog.requester")}: {selectedContract.requestedByName || t("basicContracts.dialog.unknown")} {formatDate(selectedContract.createdAt)}
{/* 뷰어 영역 - 남은 공간 모두 사용 */}
handleSurveyComplete(selectedContract.id)} onSignatureComplete={() => handleSignatureComplete(selectedContract.id)} onGtcCommentStatusChange={(hasComments, commentCount, reviewStatus, isComplete) => handleGtcCommentStatusChange(selectedContract.id, hasComments, commentCount, reviewStatus, isComplete) } mode={mode} t={t} />
{/* 고정 푸터 - 동적 버튼 */}
{/* 현재 계약서가 완료된 경우 */} {currentContractStatus?.status === 'completed' ? (

이 계약서는 이미 {isBuyerMode ? "승인이" : "서명이"} 완료되었습니다

) : currentContractStatus?.status === 'error' ? (

{isBuyerMode ? "승인" : "서명"} 처리 중 오류가 발생했습니다. 다시 시도해주세요.

) : ( <> {/* 완료 조건 안내 메시지 */}

{isBuyerMode ? "계약서에 구매자 서명을 완료해주세요." : t("basicContracts.dialog.signWarning") }

{/* 완료 상태 체크리스트 */} {!isBuyerMode && (
{selectedContract.templateName?.includes('준법') && ( 설문조사 {surveyCompletionStatus[selectedContract.id] ? '완료' : '미완료'} )} {selectedContract.templateName?.includes('GTC') && ( 조항검토 {(gtcCommentStatus[selectedContract.id]?.isComplete === true) ? '완료' : `미완료 (코멘트 ${gtcCommentStatus[selectedContract.id]?.commentCount || 0}개)`} )} 서명 {signatureStatus[selectedContract.id] ? '완료' : '미완료'}
)} {/* 구매자 모드의 간소화된 체크리스트 */} {isBuyerMode && (
구매자 서명 {signatureStatus[selectedContract.id] ? '완료' : '미완료'}
)}
)}
{/* 동적 버튼 영역 */}
{allCompleted ? ( // 모든 계약서 완료시 ) : currentContractStatus?.status === 'completed' ? ( // 현재 계약서가 완료된 경우 ) : ( // 현재 계약서를 서명해야 하는 경우 )}
) : (
{isBuyerMode ? ( ) : ( )}

{t("basicContracts.dialog.selectDocument")}

{isBuyerMode ? "승인할 계약서를 선택해주세요." : t("basicContracts.dialog.selectDocumentDescription") }

)}
); }