diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-17 08:26:41 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-17 08:26:41 +0000 |
| commit | d19cca70ad1689807192a8784efc3091bf677816 (patch) | |
| tree | e3041a53dae9346eebada10e52c0d6e8d03f27ef /lib/basic-contract | |
| parent | 4a077640becddf65ac2dc98451c5c83aa70108f8 (diff) | |
(임수민) 서명란 수정
Diffstat (limited to 'lib/basic-contract')
| -rw-r--r-- | lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | 247 |
1 files changed, 127 insertions, 120 deletions
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx index eeebae4e..4d98675b 100644 --- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx +++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx @@ -9,7 +9,7 @@ import React, { } from "react"; import { WebViewerInstance } from "@pdftron/webviewer"; import { Loader2, FileText, ClipboardList, AlertTriangle, FileSignature, Target, CheckCircle2, BookOpen } from "lucide-react"; -import { toast } from "sonner"; +import { useToast } from "@/hooks/use-toast"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -24,7 +24,7 @@ import { import { getActiveSurveyTemplate, getVendorSignatureFile, type SurveyTemplateWithQuestions } from '../service'; import { useConditionalSurvey } from '../vendor-table/survey-conditional'; import { SurveyComponent } from './SurveyComponent'; -import { GtcClausesComponent } from './GtcClausesComponent'; +import { AgreementCommentList } from '../agreement-comments/agreement-comment-list'; import { getBuyerSignatureFileWithFallback } from "@/lib/shi-signature/buyer-signature"; interface FileInfo { @@ -181,7 +181,7 @@ class AutoSignatureFieldDetector { const fieldName = `signature_at_text_${Date.now()}_${fieldNames.length}`; const signatureY = y + textHeight + 5; - // 첫 번째 필드의 위치 정보만 저장 (서명란으로 이동 기능용) + // 첫 번째 필드의 위치 정보만 저장 if (fieldNames.length === 0) { this.location = { pageNumber, @@ -355,7 +355,11 @@ const applyBuyerSignatureAutomatically = async (instance: WebViewerInstance) => }; -function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendor' | 'buyer' = 'vendor') { +function useAutoSignatureFields( + instance: WebViewerInstance | null, + mode: 'vendor' | 'buyer' = 'vendor', + toastFn?: ReturnType<typeof useToast>['toast'] +) { const [signatureFields, setSignatureFields] = useState<string[]>([]); const [isProcessing, setIsProcessing] = useState(false); const [error, setError] = useState<string | null>(null); @@ -431,7 +435,11 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo processedDocumentRef.current = documentId; } - toast.success(`📋 ${fieldNames.length}개의 기존 서명 필드를 확인했습니다.`); + if (toastFn) { + toastFn({ + title: `📋 ${fieldNames.length}개의 기존 서명 필드를 확인했습니다.` + }); + } return; } @@ -455,23 +463,23 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo const hasTextBasedField = fields.some(field => field.startsWith('signature_at_text_')); const hasSimpleField = fields.some(field => field.startsWith('simple_signature_')); - if (hasTextBasedField) { - const searchText = mode === 'buyer' ? '삼성중공업_서명란' : '협력업체_서명란'; - toast.success(`📝 "${searchText}" 위치에 서명 필드가 생성되었습니다.`, { - description: "해당 텍스트 근처의 파란색 영역에서 서명해주세요.", - icon: <FileSignature className="h-4 w-4 text-blue-500" />, - duration: 5000 - }); - } else if (hasSimpleField) { - const positionMessage = mode === 'buyer' - ? "마지막 페이지 왼쪽 하단의 파란색 영역에서 서명해주세요." - : "마지막 페이지 하단의 파란색 영역에서 서명해주세요."; - - toast.info("📝 기본 위치에 서명 필드가 생성되었습니다.", { - description: `검색 텍스트를 찾을 수 없어 ${positionMessage}`, - icon: <FileSignature className="h-4 w-4 text-blue-500" />, - duration: 5000 - }); + if (toastFn) { + if (hasTextBasedField) { + const searchText = mode === 'buyer' ? '삼성중공업_서명란' : '협력업체_서명란'; + toastFn({ + title: `📝 "${searchText}" 위치에 서명 필드가 생성되었습니다.`, + description: "해당 텍스트 근처의 파란색 영역에서 서명해주세요." + }); + } else if (hasSimpleField) { + const positionMessage = mode === 'buyer' + ? "마지막 페이지 왼쪽 하단의 파란색 영역에서 서명해주세요." + : "마지막 페이지 하단의 파란색 영역에서 서명해주세요."; + + toastFn({ + title: "📝 기본 위치에 서명 필드가 생성되었습니다.", + description: `검색 텍스트를 찾을 수 없어 ${positionMessage}` + }); + } } } @@ -480,10 +488,12 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo const errorMessage = error instanceof Error ? error.message : "서명 필드 처리에 실패했습니다."; setError(errorMessage); - toast.info("수동 서명 모드", { - description: "문서에서 서명할 위치를 직접 클릭해주세요.", - icon: <FileSignature className="h-4 w-4 text-blue-500" /> - }); + if (toastFn) { + toastFn({ + title: "수동 서명 모드", + description: "문서에서 서명할 위치를 직접 클릭해주세요." + }); + } } finally { setIsProcessing(false); processingRef.current = false; @@ -528,7 +538,7 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo processedDocumentRef.current = null; handlerRef.current = null; }; - }, [instance, mode]); // mode 의존성 추가 + }, [instance, mode, toastFn]); // mode, toastFn 의존성 추가 // 컴포넌트 언마운트 시 정리 useEffect(() => { @@ -550,53 +560,6 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo }; } -// 서명란으로 이동하는 함수 추가 -const navigateToSignatureField = ( - instance: WebViewerInstance | null, - location: SignatureFieldLocation | null -) => { - if (!instance || !location) { - toast.error("서명란 위치를 찾을 수 없습니다."); - return; - } - - try { - const { documentViewer, annotationManager } = instance.Core; - - // 1. 해당 페이지로 이동 - documentViewer.setCurrentPage(location.pageNumber); - - // 2. 서명 필드 위젯 찾기 - const annotations = annotationManager.getAnnotationsList(); - const signatureWidget = annotations.find( - (annot: any) => annot instanceof instance.Core.Annotations.SignatureWidgetAnnotation && - annot.getField()?.name === location.fieldName - ); - - if (signatureWidget) { - // 위젯이 있으면 선택하여 자동으로 해당 위치로 스크롤 - annotationManager.selectAnnotation(signatureWidget); - - // 추가로 스크롤 위치 조정 - setTimeout(() => { - try { - // annotation을 다시 선택하여 스크롤 보장 - annotationManager.selectAnnotation(signatureWidget); - } catch (scrollError) { - console.warn("스크롤 조정 실패:", scrollError); - } - }, 300); - - toast.success(`서명란으로 이동했습니다. (페이지 ${location.pageNumber})`); - } else { - // 위젯을 찾지 못한 경우 페이지만 이동 - toast.info(`페이지 ${location.pageNumber}로 이동했습니다.`); - } - } catch (error) { - console.error("서명란으로 이동 실패:", error); - toast.error("서명란으로 이동하는데 실패했습니다."); - } -}; // XFDF 기반 서명 감지 function useSignatureDetection(instance: WebViewerInstance | null, onSignatureComplete?: () => void) { const [hasValidSignature, setHasValidSignature] = useState(false); @@ -730,6 +693,7 @@ export function BasicContractSignViewer({ mode = 'vendor', // 기본값 vendor t = (key: string) => key, }: BasicContractSignViewerProps) { + const { toast } = useToast(); const [fileLoading, setFileLoading] = useState<boolean>(true); const [activeTab, setActiveTab] = useState<string>("main"); @@ -768,7 +732,7 @@ export function BasicContractSignViewer({ hasSignatureFields, error: autoSignError, signatureLocation // 위치 정보 추가 - } = useAutoSignatureFields(webViewerInstance.current || instance, mode); + } = useAutoSignatureFields(webViewerInstance.current || instance, mode, toast); const { hasValidSignature } = useSignatureDetection(webViewerInstance.current || instance, onSignatureComplete); @@ -788,7 +752,7 @@ export function BasicContractSignViewer({ }); } - // 구매자 모드일 때는 추가 파일, 설문조사, 조항 검토 탭 제외 + // 구매자 모드일 때는 추가 파일, 설문조사, 협의 코멘트 탭 제외 if (mode === 'buyer') { return files; // 메인 계약서 파일만 반환 } @@ -814,7 +778,7 @@ export function BasicContractSignViewer({ if (isGTCTemplate) { files.push({ path: "", - name: "조항 검토", + name: "협의 코멘트", type: "clauses", }); } @@ -1056,19 +1020,28 @@ export function BasicContractSignViewer({ if (showToast) { setFileLoading(false); - toast.error(errorMessage); + toast({ + title: errorMessage, + variant: "destructive" + }); } }); }).catch((error) => { console.error("📛 WebViewer 초기화 실패:", error); setFileLoading(false); - toast.error("뷰어 초기화에 실패했습니다."); + toast({ + title: "뷰어 초기화에 실패했습니다.", + variant: "destructive" + }); }); }).catch((error) => { console.error("📛 WebViewer 모듈 로드 실패:", error); setFileLoading(false); - toast.error("뷰어 모듈을 불러오는데 실패했습니다."); + toast({ + title: "뷰어 모듈을 불러오는데 실패했습니다.", + variant: "destructive" + }); }); }; @@ -1139,7 +1112,10 @@ export function BasicContractSignViewer({ msg = ""; } } - if (msg) toast.error(msg); + if (msg) toast({ + title: msg, + variant: "destructive" + }); } finally { setFileLoading(false); } @@ -1250,10 +1226,13 @@ export function BasicContractSignViewer({ const { documentViewer, annotationManager } = currentInstance.Core; const doc = documentViewer.getDocument(); - if (!doc) { - toast.error("문서가 로드되지 않았습니다."); - return; - } + if (!doc) { + toast({ + title: "문서가 로드되지 않았습니다.", + variant: "destructive" + }); + return; + } const formData = await collectFormData(currentInstance); @@ -1266,14 +1245,20 @@ export function BasicContractSignViewer({ // 구매자 모드일 때는 설문조사와 GTC 검증 건너뛰기 if (mode !== 'buyer') { if (isComplianceTemplate && !surveyData.completed) { - toast.error("준법 설문조사를 먼저 완료해주세요."); + toast({ + title: "준법 설문조사를 먼저 완료해주세요.", + variant: "destructive" + }); setActiveTab('survey'); return; } if (isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete) { - toast.error("GTC 조항에 미해결 코멘트가 있어 서명할 수 없습니다."); - toast.info("모든 코멘트를 삭제하거나 협의를 완료한 후 서명해주세요."); + toast({ + title: "미해결 코멘트가 있어 서명할 수 없습니다.", + description: "모든 코멘트를 삭제하거나 협의를 완료한 후 서명해주세요.", + variant: "destructive" + }); setActiveTab('clauses'); return; } @@ -1283,14 +1268,19 @@ export function BasicContractSignViewer({ await onSign(documentData, { formData, surveyData, signatureFields }); } else { const actionText = mode === 'buyer' ? "최종승인" : "서명"; - toast.success(`계약서가 성공적으로 ${actionText}되었습니다.`); + toast({ + title: `계약서가 성공적으로 ${actionText}되었습니다.` + }); } handleClose(); } catch (error) { console.error(`📛 ${mode === 'buyer' ? '최종승인' : '서명'} 저장 실패:`, error); const actionText = mode === 'buyer' ? "최종승인" : "서명"; - toast.error(`${actionText}을 저장하는데 실패했습니다.`); + toast({ + title: `${actionText}을 저장하는데 실패했습니다.`, + variant: "destructive" + }); } }; @@ -1326,17 +1316,6 @@ export function BasicContractSignViewer({ <Target className="h-3 w-3 mr-1" /> {signatureFields.length}개 서명 필드 자동 생성됨 {mode === 'buyer' ? '(왼쪽 하단)' : ''} </Badge> - {signatureLocation && ( - <Button - variant="ghost" - size="sm" - className="h-6 px-2 text-xs hover:bg-blue-50" - onClick={() => navigateToSignatureField(currentInstance, signatureLocation)} - > - <Target className="h-3 w-3 mr-1" /> - 서명란으로 이동 - </Button> - )} </> ) : null} @@ -1420,15 +1399,27 @@ export function BasicContractSignViewer({ <div className={`absolute inset-0 p-3 ${activeTab === 'clauses' ? 'block' : 'hidden'}`} > - {/* GTC 조항 컴포넌트 */} - <GtcClausesComponent - contractId={contractId} - onCommentStatusChange={(hasComments, commentCount, reviewStatus, isComplete) => { - setGtcCommentStatus({ hasComments, commentCount, reviewStatus, isComplete }); - onGtcCommentStatusChange?.(hasComments, commentCount, reviewStatus, isComplete); - }} - t={t} - /> + {/* 새로운 협의 코멘트 리스트 */} + {contractId ? ( + <div className="h-full"> + <AgreementCommentList + basicContractId={contractId} + currentUserType={mode === 'vendor' ? 'Vendor' : 'SHI'} + readOnly={false} + onCommentCountChange={(count) => { + const hasComments = count > 0; + const reviewStatus = hasComments ? 'negotiating' : 'draft'; + const isComplete = false; + setGtcCommentStatus({ hasComments, commentCount: count, reviewStatus, isComplete }); + onGtcCommentStatusChange?.(hasComments, count, reviewStatus, isComplete); + }} + /> + </div> + ) : ( + <div className="flex items-center justify-center h-full text-gray-500"> + <p>계약서 정보를 불러올 수 없습니다. (contractId: {String(contractId)})</p> + </div> + )} </div> <div @@ -1511,7 +1502,7 @@ export function BasicContractSignViewer({ <span className="block mt-1 text-amber-600">📋 준법 설문조사를 먼저 완료해주세요.</span> )} {mode !== 'buyer' && isGTCTemplate && ( - <span className="block mt-1 text-blue-600">📋 GTC 조항을 검토하고 코멘트가 없는지 확인해주세요.</span> + <span className="block mt-1 text-blue-600">📋 협의 코멘트를 확인하고 모든 협의가 완료되었는지 확인해주세요.</span> )} {hasSignatureFields && ( <span className="block mt-1 text-green-600"> @@ -1525,17 +1516,17 @@ export function BasicContractSignViewer({ )} {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && ( <span className="block mt-1 text-red-600"> - ⚠️ GTC 조항에 {gtcCommentStatus.commentCount}개의 코멘트가 있어 서명할 수 없습니다. + ⚠️ {gtcCommentStatus.commentCount}개의 코멘트가 있어 서명할 수 없습니다. </span> )} {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete && ( <span className="block mt-1 text-red-600"> - ⚠️ GTC 조항에 {gtcCommentStatus.commentCount}개의 미해결 코멘트가 있어 서명할 수 없습니다. + ⚠️ {gtcCommentStatus.commentCount}개의 미해결 코멘트가 있어 서명할 수 없습니다. </span> )} {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && gtcCommentStatus.isComplete && ( <span className="block mt-1 text-green-600"> - ✅ GTC 조항 협의가 완료되어 서명 가능합니다. + ✅ 협의가 완료되어 서명 가능합니다. </span> )} </DialogDescription> @@ -1603,12 +1594,28 @@ export function BasicContractSignViewer({ </div> <div className={`absolute inset-0 p-3 ${activeTab === 'clauses' ? 'block' : 'hidden'}`}> - {/* GTC 조항 컴포넌트 */} - <GtcClausesComponent - contractId={contractId} - onCommentStatusChange={handleGtcCommentStatusChange} // 메모이제이션된 콜백 사용 - t={t} - /> + {/* 새로운 협의 코멘트 리스트 */} + {contractId ? ( + <div className="h-full"> + <AgreementCommentList + basicContractId={contractId} + currentUserType={mode === 'vendor' ? 'Vendor' : 'SHI'} + readOnly={false} + onCommentCountChange={(count) => { + handleGtcCommentStatusChange?.( + count > 0, + count, + count > 0 ? 'negotiating' : 'draft', + false + ); + }} + /> + </div> + ) : ( + <div className="flex items-center justify-center h-full text-gray-500"> + <p>계약서 정보를 불러올 수 없습니다. (contractId: {String(contractId)})</p> + </div> + )} </div> <div className={`absolute inset-0 ${activeTab !== 'survey' && activeTab !== 'clauses' ? 'block' : 'hidden'}`}> |
