diff options
Diffstat (limited to 'lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx')
| -rw-r--r-- | lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx | 237 |
1 files changed, 127 insertions, 110 deletions
diff --git a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx index f70bed94..fa68c9c8 100644 --- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx @@ -51,9 +51,9 @@ interface BasicContractSignDialogProps { onOpenChange?: (open: boolean) => void; } -export function BasicContractSignDialog({ - contracts, - onSuccess, +export function BasicContractSignDialog({ + contracts, + onSuccess, hasSelectedRows = false, mode = 'vendor', onBuyerSignComplete, @@ -68,19 +68,26 @@ export function BasicContractSignDialog({ const [instance, setInstance] = React.useState<null | WebViewerInstance>(null); const [searchTerm, setSearchTerm] = React.useState(""); const [isSubmitting, setIsSubmitting] = React.useState(false); - + // 추가된 state들 const [additionalFiles, setAdditionalFiles] = React.useState<any[]>([]); const [isLoadingAttachments, setIsLoadingAttachments] = React.useState(false); - + // 계약서 상태 관리 const [contractStatuses, setContractStatuses] = React.useState<ContractStatus[]>([]); - + // 서명/설문/GTC 코멘트 완료 상태 관리 const [surveyCompletionStatus, setSurveyCompletionStatus] = React.useState<Record<number, boolean>>({}); const [signatureStatus, setSignatureStatus] = React.useState<Record<number, boolean>>({}); - const [gtcCommentStatus, setGtcCommentStatus] = React.useState<Record<number, { hasComments: boolean; commentCount: number }>>({}); - + const [gtcCommentStatus, setGtcCommentStatus] = React.useState<Record<number, { + hasComments: boolean; + commentCount: number; + reviewStatus?: string; + isComplete?: boolean; + }>>({}); + + console.log(gtcCommentStatus, "gtcCommentStatus") + const router = useRouter() // 실제 사용할 open 상태 (외부 제어가 있으면 외부 상태 사용, 없으면 내부 상태 사용) @@ -91,7 +98,7 @@ export function BasicContractSignDialog({ const isBuyerMode = mode === 'buyer'; const dialogTitle = isBuyerMode ? "구매자 최종승인 서명" : t("basicContracts.dialog.title"); const signButtonText = isBuyerMode ? "최종승인 완료" : "서명 완료 및 저장"; - + // 버튼 비활성화 조건 const isButtonDisabled = !hasSelectedRows || contracts.length === 0; @@ -107,27 +114,31 @@ export function BasicContractSignDialog({ }; // 현재 선택된 계약서의 서명 완료 가능 여부 확인 - const canCompleteCurrentContract = React.useMemo(() => { - if (!selectedContract) return false; - - const contractId = selectedContract.id; - - // 구매자 모드에서는 설문조사나 GTC 체크 불필요 - 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; - const gtcCompleted = isGTCTemplate ? (gtcCommentStatus[contractId]?.hasComments !== true) : true; +const canCompleteCurrentContract = React.useMemo(() => { + if (!selectedContract) return false; + + const contractId = selectedContract.id; + + if (isBuyerMode) { const signatureCompleted = signatureStatus[contractId] === true; - - return surveyCompleted && gtcCompleted && signatureCompleted; - }, [selectedContract, surveyCompletionStatus, signatureStatus, gtcCommentStatus, isBuyerMode]); + 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(() => { @@ -147,7 +158,7 @@ export function BasicContractSignDialog({ const allCompleted = completedCount === totalCount && totalCount > 0; // 현재 선택된 계약서의 상태 - const currentContractStatus = selectedContract + const currentContractStatus = selectedContract ? contractStatuses.find(status => status.id === selectedContract.id) : null; @@ -155,7 +166,7 @@ export function BasicContractSignDialog({ 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; }; @@ -169,14 +180,14 @@ export function BasicContractSignDialog({ ); if (!confirmClose) return; } - + // 외부 제어가 있으면 외부 콜백 호출, 없으면 내부 상태 업데이트 if (isControlledExternally && externalOnOpenChange) { externalOnOpenChange(isOpen); } else { setInternalOpen(isOpen); } - + if (!isOpen) { // 다이얼로그 닫을 때 상태 초기화 setSelectedContract(null); @@ -226,7 +237,7 @@ export function BasicContractSignDialog({ } } }, [open, contracts, selectedContract, contractStatuses]); - + // 추가 파일 가져오기 useEffect (구매자 모드에서는 스킵) React.useEffect(() => { if (isBuyerMode) { @@ -285,11 +296,17 @@ export function BasicContractSignDialog({ }, []); // GTC 코멘트 상태 변경 콜백 함수 - const handleGtcCommentStatusChange = React.useCallback((contractId: number, hasComments: boolean, commentCount: number) => { - console.log(`📋 GTC 코멘트 상태 변경: 계약서 ${contractId}, 코멘트 ${commentCount}개`); + 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 } + [contractId]: { hasComments, commentCount, reviewStatus, isComplete } })); }, []); @@ -300,10 +317,10 @@ export function BasicContractSignDialog({ // 서명 완료 가능 여부 재확인 if (!canCompleteCurrentContract) { const contractId = selectedContract.id; - + if (isBuyerMode) { const signatureCompleted = signatureStatus[contractId] === true; - + if (!signatureCompleted) { toast.error("계약서에 서명을 먼저 완료해주세요.", { description: "문서의 서명 필드에 서명해주세요.", @@ -316,9 +333,9 @@ export function BasicContractSignDialog({ const isComplianceTemplate = selectedContract.templateName?.includes('준법'); const isGTCTemplate = selectedContract.templateName?.includes('GTC'); const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true; - const gtcCompleted = isGTCTemplate ? (gtcCommentStatus[contractId]?.hasComments !== true) : true; + const gtcCompleted = isGTCTemplate ? (gtcCommentStatus[contractId]?.isComplete !== true) : true; const signatureCompleted = signatureStatus[contractId] === true; - + if (!surveyCompleted) { toast.error("준법 설문조사를 먼저 완료해주세요.", { description: "설문조사 탭에서 모든 필수 항목을 완료해주세요.", @@ -326,7 +343,7 @@ export function BasicContractSignDialog({ }); return; } - + if (!gtcCompleted) { toast.error("GTC 조항에 코멘트가 있어 서명할 수 없습니다.", { description: "조항 검토 탭에서 모든 코멘트를 삭제하거나 협의를 완료해주세요.", @@ -334,7 +351,7 @@ export function BasicContractSignDialog({ }); return; } - + if (!signatureCompleted) { toast.error("계약서에 서명을 먼저 완료해주세요.", { description: "문서의 서명 필드에 서명해주세요.", @@ -343,7 +360,7 @@ export function BasicContractSignDialog({ return; } } - + return; } @@ -376,9 +393,9 @@ export function BasicContractSignDialog({ if (result.success) { // 성공시 해당 계약서 상태를 완료로 업데이트 - setContractStatuses(prev => - prev.map(status => - status.id === selectedContract.id + setContractStatuses(prev => + prev.map(status => + status.id === selectedContract.id ? { ...status, status: 'completed' as const } : status ) @@ -413,9 +430,9 @@ export function BasicContractSignDialog({ router.refresh(); } else { // 실패시 에러 상태 업데이트 - setContractStatuses(prev => - prev.map(status => - status.id === selectedContract.id + setContractStatuses(prev => + prev.map(status => + status.id === selectedContract.id ? { ...status, status: 'error' as const, errorMessage: result.message } : status ) @@ -432,26 +449,26 @@ export function BasicContractSignDialog({ 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 + setContractStatuses(prev => + prev.map(status => + status.id === selectedContract.id ? { ...status, status: 'completed' as const } : status ) @@ -481,9 +498,9 @@ export function BasicContractSignDialog({ router.refresh(); } else { // 실패시 에러 상태 업데이트 - setContractStatuses(prev => - prev.map(status => - status.id === selectedContract.id + setContractStatuses(prev => + prev.map(status => + status.id === selectedContract.id ? { ...status, status: 'error' as const, errorMessage: result.error } : status ) @@ -497,11 +514,11 @@ export function BasicContractSignDialog({ } } catch (error) { console.error("서명 완료 중 오류:", error); - + // 에러 상태 업데이트 - setContractStatuses(prev => - prev.map(status => - status.id === selectedContract.id + setContractStatuses(prev => + prev.map(status => + status.id === selectedContract.id ? { ...status, status: 'error' as const, errorMessage: '서명 처리 중 오류가 발생했습니다' } : status ) @@ -519,10 +536,10 @@ export function BasicContractSignDialog({ if (onSuccess) { onSuccess(); } - const successMessage = isBuyerMode - ? "모든 계약서 최종승인이 완료되었습니다!" + const successMessage = isBuyerMode + ? "모든 계약서 최종승인이 완료되었습니다!" : "모든 계약서 서명이 완료되었습니다!"; - + toast.success(successMessage, { description: "계약서 관리 페이지가 새고침됩니다.", icon: <Trophy className="h-5 w-5 text-yellow-500" /> @@ -540,33 +557,33 @@ export function BasicContractSignDialog({ disabled={isButtonDisabled} className={cn( "gap-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed", - isBuyerMode - ? "hover:bg-green-50 hover:text-green-600 hover:border-green-200" + isBuyerMode + ? "hover:bg-green-50 hover:text-green-600 hover:border-green-200" : "hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200" )} > {isBuyerMode ? ( - <Shield - className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-green-500'}`} - aria-hidden="true" + <Shield + className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-green-500'}`} + aria-hidden="true" /> ) : ( - <Upload - className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-blue-500'}`} - aria-hidden="true" + <Upload + className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-blue-500'}`} + aria-hidden="true" /> )} <span className="hidden sm:inline flex items-center"> - {isBuyerMode ? "구매자 승인" : t("basicContracts.toolbar.sign")} + {isBuyerMode ? "구매자 서명" : t("basicContracts.toolbar.sign")} {contracts.length > 0 && !isButtonDisabled && ( - <Badge - variant="secondary" + <Badge + variant="secondary" className={cn( "ml-2", - isBuyerMode - ? "bg-green-100 text-green-700 hover:bg-green-200" + isBuyerMode + ? "bg-green-100 text-green-700 hover:bg-green-200" : "bg-blue-100 text-blue-700 hover:bg-blue-200" - )}최 + )} > {contracts.length} </Badge> @@ -582,12 +599,12 @@ export function BasicContractSignDialog({ {/* 서명 다이얼로그 */} <Dialog open={open} onOpenChange={handleOpenChange}> - <DialogContent className="max-w-7xl w-[95vw] h-[90vh] p-0 flex flex-col overflow-hidden" style={{width:'95vw', maxWidth:'95vw'}}> + <DialogContent className="max-w-7xl w-[95vw] h-[90vh] p-0 flex flex-col overflow-hidden" style={{ width: '95vw', maxWidth: '95vw' }}> {/* 고정 헤더 - 진행 상황 표시 */} <DialogHeader className={cn( "px-6 py-4 border-b flex-shrink-0", - isBuyerMode - ? "bg-gradient-to-r from-green-50 to-emerald-50" + isBuyerMode + ? "bg-gradient-to-r from-green-50 to-emerald-50" : "bg-gradient-to-r from-blue-50 to-purple-50" )}> <DialogTitle className="text-xl font-bold flex items-center justify-between text-gray-800"> @@ -599,12 +616,12 @@ export function BasicContractSignDialog({ )} {dialogTitle} {/* 진행 상황 표시 */} - <Badge - variant="outline" + <Badge + variant="outline" className={cn( "ml-3", - isBuyerMode - ? "bg-green-50 text-green-700 border-green-200" + isBuyerMode + ? "bg-green-50 text-green-700 border-green-200" : "bg-blue-50 text-blue-700 border-blue-200" )} > @@ -615,7 +632,7 @@ export function BasicContractSignDialog({ <Loader2 className="ml-2 h-4 w-4 animate-spin text-blue-500" /> )} </div> - + {allCompleted && ( <Badge variant="default" className="bg-green-100 text-green-700 border-green-200"> <Trophy className="h-4 w-4 mr-1" /> @@ -635,8 +652,8 @@ export function BasicContractSignDialog({ <div className={cn( "h-2 rounded-full transition-all duration-500", - isBuyerMode - ? "bg-gradient-to-r from-green-500 to-emerald-500" + isBuyerMode + ? "bg-gradient-to-r from-green-500 to-emerald-500" : "bg-gradient-to-r from-blue-500 to-green-500" )} style={{ width: `${(completedCount / totalCount) * 100}%` }} @@ -677,14 +694,14 @@ export function BasicContractSignDialog({ 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 ( <Button key={contract.id} @@ -723,7 +740,7 @@ export function BasicContractSignDialog({ </Badge> )} </span> - + {/* 상태 표시 */} {isCompleted ? ( <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 text-xs ml-2 flex-shrink-0"> @@ -741,7 +758,7 @@ export function BasicContractSignDialog({ </Badge> )} </div> - + {/* 완료 상태 표시 (구매자 모드에서는 간소화) */} {!isCompleted && !hasError && !isBuyerMode && ( <div className="flex items-center space-x-2 text-xs"> @@ -763,7 +780,7 @@ export function BasicContractSignDialog({ </span> </div> )} - + {/* 구매자 모드의 간소화된 상태 표시 */} {!isCompleted && !hasError && isBuyerMode && ( <div className="flex items-center space-x-2 text-xs"> @@ -773,7 +790,7 @@ export function BasicContractSignDialog({ </span> </div> )} - + {/* 두 번째 줄: 사용자 + 날짜 */} <div className="flex items-center justify-between text-xs text-gray-500"> <div className="flex items-center min-w-0"> @@ -815,7 +832,7 @@ export function BasicContractSignDialog({ <FileText className="h-4 w-4 mr-2 text-blue-500" /> )} {selectedContract.templateName || t("basicContracts.dialog.document")} - + {/* 현재 계약서 상태 표시 */} {currentContractStatus?.status === 'completed' ? ( <Badge variant="outline" className="ml-2 bg-green-50 text-green-700 border-green-200"> @@ -852,7 +869,7 @@ export function BasicContractSignDialog({ GTC 계약서 </Badge> )} - + {/* 비밀유지 계약서인 경우 추가 파일 수 표시 (구매자 모드가 아닐 때만) */} {!isBuyerMode && selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && ( <Badge variant="outline" className="ml-2 bg-blue-50 text-blue-700 border-blue-200"> @@ -871,7 +888,7 @@ export function BasicContractSignDialog({ </span> </div> </div> - + {/* 뷰어 영역 - 남은 공간 모두 사용 */} <div className="flex-1 min-h-0 overflow-hidden"> <BasicContractSignViewer @@ -884,8 +901,8 @@ export function BasicContractSignDialog({ setInstance={setInstance} onSurveyComplete={() => handleSurveyComplete(selectedContract.id)} onSignatureComplete={() => handleSignatureComplete(selectedContract.id)} - onGtcCommentStatusChange={(hasComments, commentCount) => - handleGtcCommentStatusChange(selectedContract.id, hasComments, commentCount) + onGtcCommentStatusChange={(hasComments, commentCount, reviewStatus, isComplete) => + handleGtcCommentStatusChange(selectedContract.id, hasComments, commentCount, reviewStatus, isComplete) } mode={mode} t={t} @@ -912,12 +929,12 @@ export function BasicContractSignDialog({ <div className="flex flex-col space-y-1"> <p className="text-sm text-gray-600 flex items-center"> <AlertCircle className="h-4 w-4 text-yellow-500 mr-1" /> - {isBuyerMode + {isBuyerMode ? "계약서에 구매자 서명을 완료해주세요." : t("basicContracts.dialog.signWarning") } </p> - + {/* 완료 상태 체크리스트 */} {!isBuyerMode && ( <div className="flex items-center space-x-4 text-xs"> @@ -928,9 +945,9 @@ export function BasicContractSignDialog({ </span> )} {selectedContract.templateName?.includes('GTC') && ( - <span className={`flex items-center ${(gtcCommentStatus[selectedContract.id]?.hasComments !== true) ? 'text-green-600' : 'text-red-600'}`}> - <CheckCircle2 className={`h-3 w-3 mr-1 ${(gtcCommentStatus[selectedContract.id]?.hasComments !== true) ? 'text-green-500' : 'text-red-500'}`} /> - 조항검토 {(gtcCommentStatus[selectedContract.id]?.hasComments !== true) ? '완료' : + <span className={`flex items-center ${(gtcCommentStatus[selectedContract.id]?.isComplete === true) ? 'text-green-600' : 'text-red-600'}`}> + <CheckCircle2 className={`h-3 w-3 mr-1 ${(gtcCommentStatus[selectedContract.id]?.isComplete === true) ? 'text-green-500' : 'text-red-500'}`} /> + 조항검토 {(gtcCommentStatus[selectedContract.id]?.isComplete === true) ? '완료' : `미완료 (코멘트 ${gtcCommentStatus[selectedContract.id]?.commentCount || 0}개)`} </span> )} @@ -962,8 +979,8 @@ export function BasicContractSignDialog({ <Button className={cn( "gap-2 transition-colors", - isBuyerMode - ? "bg-green-600 hover:bg-green-700" + isBuyerMode + ? "bg-green-600 hover:bg-green-700" : "bg-green-600 hover:bg-green-700" )} onClick={completeAllSigns} @@ -992,7 +1009,7 @@ export function BasicContractSignDialog({ <Button className={cn( "gap-2 transition-colors", - canCompleteCurrentContract + canCompleteCurrentContract ? isBuyerMode ? "bg-green-600 hover:bg-green-700" : "bg-blue-600 hover:bg-blue-700" @@ -1043,7 +1060,7 @@ export function BasicContractSignDialog({ </div> <h3 className="text-xl font-medium text-gray-800 mb-2">{t("basicContracts.dialog.selectDocument")}</h3> <p className="text-gray-500 max-w-md"> - {isBuyerMode + {isBuyerMode ? "승인할 계약서를 선택해주세요." : t("basicContracts.dialog.selectDocumentDescription") } |
