diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-22 06:14:04 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-22 06:14:04 +0000 |
| commit | c8398b21a25497dec5304caed61103556296a849 (patch) | |
| tree | 226ccbf87176defb34a01c5d9a7f47f3152ca545 /lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | |
| parent | dbdae213e39b82ff8ee565df0774bd2f72f06140 (diff) | |
(대표님) 기본계약 서명 로직 개발
Diffstat (limited to 'lib/basic-contract/viewer/basic-contract-sign-viewer.tsx')
| -rw-r--r-- | lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | 391 |
1 files changed, 265 insertions, 126 deletions
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx index 49efb551..b92df089 100644 --- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx +++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx @@ -150,12 +150,6 @@ class AutoSignatureFieldDetector { console.log("📄 문서 확인 완료, 기존 필드 검사..."); - // ✅ 3단계: 기존 서명 필드 확인 (안전한 방법) - const existingFields = await this.checkExistingFieldsSafely(); - if (existingFields.length > 0) { - console.log(`✅ 기존 서명 필드 발견: ${existingFields.length}개`); - return existingFields; - } // ✅ 4단계: 단순 기본 서명 필드 생성 (텍스트 분석 스킵) console.log("📝 기본 서명 필드 생성..."); @@ -182,34 +176,7 @@ class AutoSignatureFieldDetector { } } - // ✅ 안전한 기존 필드 확인 (PDFDoc 접근 안함) - private async checkExistingFieldsSafely(): Promise<string[]> { - try { - const { annotationManager } = this.instance.Core; - const annotations = annotationManager.getAnnotationsList(); - - const signatureFields: string[] = []; - - for (const annotation of annotations) { - try { - if (annotation.getCustomData && annotation.getCustomData('fieldName')) { - const fieldName = annotation.getCustomData('fieldName'); - if (fieldName.includes('signature') || fieldName.includes('서명')) { - signatureFields.push(fieldName); - } - } - } catch (annotError) { - // 개별 어노테이션 에러 무시 - continue; - } - } - - return signatureFields; - } catch (error) { - console.warn("기존 필드 확인 실패 (무시):", error); - return []; - } - } + // ✅ 초간단 서명 필드 생성 (복잡한 텍스트 분석 없이) private async createSimpleSignatureField(): Promise<string> { @@ -229,28 +196,40 @@ class AutoSignatureFieldDetector { // ✅ 간단한 서명 어노테이션 생성 (PDFDoc 접근 없이) const fieldName = `simple_signature_${Date.now()}`; + + const flags = new Annotations.WidgetFlags(); + // flags.set(Annotations.WidgetFlags.REQUIRED, true); + // flags.set(Annotations.WidgetFlags.READ_ONLY, true); + + const formField = new Core.Annotations.Forms.Field( + `SignatureFormField`, + { + type: "Sig", + flags, + } + ); // 서명 위젯 어노테이션 생성 - const signatureWidget = new Annotations.SignatureWidgetAnnotation({ - appearance: Annotations.SignatureWidgetAnnotation.DefaultAppearance.MATERIAL_OUTLINE, + const signatureWidget = new Annotations.SignatureWidgetAnnotation(formField,{ + // appearance: Annotations.SignatureWidgetAnnotation.DefaultAppearance.MATERIAL_OUTLINE, Width: 150, Height: 50 }); // 위치 설정 (마지막 페이지 하단) signatureWidget.setPageNumber(pageCount); - signatureWidget.setX(pageWidth * 0.3); - signatureWidget.setY(pageHeight * 0.15); + signatureWidget.setX(pageWidth * 0.7); + signatureWidget.setY(pageHeight * 0.85); signatureWidget.setWidth(150); signatureWidget.setHeight(50); // 필드명 설정 - signatureWidget.setFieldName(fieldName); - signatureWidget.setCustomData('fieldName', fieldName); + // signatureWidget.setFieldName(fieldName); + // signatureWidget.setCustomData('fieldName', fieldName); - // 스타일 설정 - signatureWidget.StrokeColor = new Annotations.Color(0, 100, 200); // 파란색 - signatureWidget.StrokeThickness = 2; + // // 스타일 설정 + // signatureWidget.StrokeColor = new Annotations.Color(0, 100, 200); // 파란색 + // signatureWidget.StrokeThickness = 2; // 어노테이션 추가 annotationManager.addAnnotation(signatureWidget); @@ -263,47 +242,47 @@ class AutoSignatureFieldDetector { console.error("📛 간단 서명 필드 생성 실패:", error); // ✅ 최후의 수단: 텍스트 어노테이션으로 안내 - return await this.createTextGuidance(); + // return await this.createTextGuidance(); } } // ✅ 최후의 수단: 텍스트 안내 생성 - private async createTextGuidance(): Promise<string> { - try { - const { Core } = this.instance; - const { documentViewer, annotationManager, Annotations } = Core; + // private async createTextGuidance(): Promise<string> { + // try { + // const { Core } = this.instance; + // const { documentViewer, annotationManager, Annotations } = Core; - const pageCount = documentViewer.getPageCount(); - const pageWidth = documentViewer.getPageWidth(pageCount) || 612; - const pageHeight = documentViewer.getPageHeight(pageCount) || 792; + // const pageCount = documentViewer.getPageCount(); + // const pageWidth = documentViewer.getPageWidth(pageCount) || 612; + // const pageHeight = documentViewer.getPageHeight(pageCount) || 792; - // 텍스트 어노테이션으로 서명 안내 - const textAnnot = new Annotations.FreeTextAnnotation(); - textAnnot.setPageNumber(pageCount); - textAnnot.setX(pageWidth * 0.25); - textAnnot.setY(pageHeight * 0.1); - textAnnot.setWidth(pageWidth * 0.5); - textAnnot.setHeight(60); - textAnnot.setContents("👆 여기를 클릭하여 서명해주세요"); - textAnnot.FontSize = '14pt'; - textAnnot.TextColor = new Annotations.Color(255, 0, 0); // 빨간색 - textAnnot.StrokeColor = new Annotations.Color(255, 200, 200); - textAnnot.FillColor = new Annotations.Color(255, 240, 240); + // // 텍스트 어노테이션으로 서명 안내 + // const textAnnot = new Annotations.FreeTextAnnotation(); + // textAnnot.setPageNumber(pageCount); + // textAnnot.setX(pageWidth * 0.25); + // textAnnot.setY(pageHeight * 0.1); + // textAnnot.setWidth(pageWidth * 0.5); + // textAnnot.setHeight(60); + // textAnnot.setContents("👆 여기를 클릭하여 서명해주세요"); + // textAnnot.FontSize = '14pt'; + // textAnnot.TextColor = new Annotations.Color(255, 0, 0); // 빨간색 + // textAnnot.StrokeColor = new Annotations.Color(255, 200, 200); + // textAnnot.FillColor = new Annotations.Color(255, 240, 240); - const fieldName = `text_guidance_${Date.now()}`; - textAnnot.setCustomData('fieldName', fieldName); + // const fieldName = `text_guidance_${Date.now()}`; + // textAnnot.setCustomData('fieldName', fieldName); - annotationManager.addAnnotation(textAnnot); - annotationManager.redrawAnnotation(textAnnot); + // annotationManager.addAnnotation(textAnnot); + // annotationManager.redrawAnnotation(textAnnot); - console.log(`✅ 텍스트 안내 생성: ${fieldName}`); - return fieldName; + // console.log(`✅ 텍스트 안내 생성: ${fieldName}`); + // return fieldName; - } catch (error) { - console.error("📛 텍스트 안내 생성도 실패:", error); - return "manual_signature_required"; - } - } + // } catch (error) { + // console.error("📛 텍스트 안내 생성도 실패:", error); + // return "manual_signature_required"; + // } + // } } function useAutoSignatureFields(instance: WebViewerInstance | null) { @@ -692,7 +671,10 @@ useEffect(() => { { path: "/pdftronWeb", licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, - fullAPI: true + fullAPI: true , + disabledElements: [ + + ] }, viewerElement ).then((newInstance) => { @@ -737,6 +719,19 @@ useEffect(() => { newInstance.UI.setMinZoomLevel('25%'); newInstance.UI.setMaxZoomLevel('400%'); + + newInstance.UI.disableElements([ + "toolbarGroup-Annotate", + "toolbarGroup-Shapes", + "toolbarGroup-Insert", + "toolbarGroup-Edit", + "toolbarGroup-FillAndSign", + "toolbarGroup-Forms", + "saveAsButton", + "downloadButton", + + ]) + documentViewer.addEventListener('documentLoadingError', (error) => { console.error("📛 WebViewer 문서 로딩 에러:", error); @@ -1359,7 +1354,7 @@ const SignatureFieldsStatus = () => { ); }; -// 인라인 뷰어 렌더링 +// 인라인 뷰어 렌더링 부분 수정 if (!isOpen && !onClose) { return ( <div className="h-full w-full flex flex-col overflow-hidden"> @@ -1368,33 +1363,33 @@ if (!isOpen && !onClose) { <div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0"> <SignatureFieldsStatus /> <TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}> - {allFiles.map((file, index) => { - let tabId: string; - if (index === 0) { - tabId = 'main'; - } else if (file.type === 'survey') { - tabId = 'survey'; - } else { - const fileOnlyIndex = allFiles.slice(0, index).filter(f => f.type !== 'survey').length; - tabId = `file-${fileOnlyIndex}`; - } - - return ( - <TabsTrigger key={tabId} value={tabId} className="text-xs"> - <div className="flex items-center space-x-1"> - {file.type === 'survey' ? ( - <ClipboardList className="h-3 w-3" /> - ) : ( - <FileText className="h-3 w-3" /> - )} - <span className="truncate">{file.name}</span> - {file.type === 'survey' && surveyData.completed && ( - <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">완료</Badge> - )} - </div> - </TabsTrigger> - ); -})} + {allFiles.map((file, index) => { + let tabId: string; + if (index === 0) { + tabId = 'main'; + } else if (file.type === 'survey') { + tabId = 'survey'; + } else { + const fileOnlyIndex = allFiles.slice(0, index).filter(f => f.type !== 'survey').length; + tabId = `file-${fileOnlyIndex}`; + } + + return ( + <TabsTrigger key={tabId} value={tabId} className="text-xs"> + <div className="flex items-center space-x-1"> + {file.type === 'survey' ? ( + <ClipboardList className="h-3 w-3" /> + ) : ( + <FileText className="h-3 w-3" /> + )} + <span className="truncate">{file.name}</span> + {file.type === 'survey' && surveyData.completed && ( + <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">완료</Badge> + )} + </div> + </TabsTrigger> + ); + })} </TabsList> </div> @@ -1408,37 +1403,55 @@ if (!isOpen && !onClose) { <div className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`} > - <div - ref={viewer} - className="w-full h-full" - style={{ position: 'relative', minHeight: '400px' }} - > - {fileLoading && ( - <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10"> - <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> - <p className="text-sm text-muted-foreground">문서 로딩 중...</p> - </div> - )} + {/* ✅ 수정: 동일한 구조로 통일하고 스크롤 활성화 */} + <div className="w-full h-full overflow-auto"> + <div + ref={viewer} + className="w-full h-full min-h-[400px]" + style={{ + position: 'relative', + // ✅ WebViewer가 스크롤을 제어하도록 설정 + overflow: 'visible' + }} + > + {fileLoading && ( + <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> + <p className="text-sm text-muted-foreground">문서 로딩 중...</p> + </div> + )} + </div> </div> </div> </div> </Tabs> ) : ( - <div className="h-full w-full relative"> - <div className="absolute top-2 left-2 z-10"> + // ✅ 수정: Tabs가 없는 경우도 동일한 구조로 변경 + <div className="h-full w-full flex flex-col"> + <div className="flex-shrink-0 p-2"> <SignatureFieldsStatus /> </div> - <div - ref={viewer} - className="absolute inset-0" - style={{ position: 'relative', minHeight: '400px' }} - > - {fileLoading && ( - <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10"> - <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> - <p className="text-sm text-muted-foreground">문서 로딩 중...</p> + <div className="flex-1 min-h-0 overflow-hidden relative"> + <div className="absolute inset-0"> + <div className="w-full h-full overflow-auto"> + <div + ref={viewer} + className="w-full h-full min-h-[400px]" + style={{ + position: 'relative', + // ✅ WebViewer가 스크롤을 제어하도록 설정 + overflow: 'visible' + }} + > + {fileLoading && ( + <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> + <p className="text-sm text-muted-foreground">문서 로딩 중...</p> + </div> + )} + </div> </div> - )} + </div> </div> </div> )} @@ -1446,6 +1459,132 @@ if (!isOpen && !onClose) { ); } +// 다이얼로그 뷰어 렌더링 부분도 동일하게 수정 +return ( + <Dialog open={showDialog} onOpenChange={handleClose}> + <DialogContent className="w-[90vw] max-w-6xl h-[90vh] flex flex-col p-0"> + <DialogHeader className="px-6 py-4 border-b flex-shrink-0"> + <DialogTitle className="flex items-center justify-between"> + <span>기본계약서 서명</span> + <SignatureFieldsStatus /> + </DialogTitle> + <DialogDescription> + 계약서를 확인하고 서명을 진행해주세요. + {isComplianceTemplate && ( + <span className="block mt-1 text-amber-600">📋 준법 설문조사를 먼저 완료해주세요.</span> + )} + {isNDATemplate && additionalFiles.length > 0 && ( + <span className="block mt-1 text-blue-600">📎 첨부서류 {additionalFiles.length}개를 각 탭에서 확인해주세요.</span> + )} + {hasSignatureFields && ( + <span className="block mt-1 text-green-600"> + 🎯 서명 위치가 자동으로 감지되었습니다. + {signatureFields.some(f => f.includes('_text')) && ( + <span className="block text-sm text-amber-600"> + 💡 빨간색 텍스트로 표시된 영역을 찾아 서명해주세요. + </span> + )} + {signatureFields.some(f => f.startsWith('default_signature_')) && !signatureFields.some(f => f.includes('_text')) && ( + <span className="block text-sm text-amber-600"> + 💡 마지막 페이지 하단의 핑크색 영역에서 서명해주세요. + </span> + )} + </span> + )} + {autoSignError && ( + <span className="block mt-1 text-red-600">⚠️ 자동 서명 필드 생성 실패 - 수동으로 서명 위치를 클릭해주세요.</span> + )} + </DialogDescription> + </DialogHeader> + + <div className="flex-1 min-h-0 overflow-hidden"> + {allFiles.length > 1 ? ( + <Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col"> + <div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0"> + <TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}> + {allFiles.map((file, index) => { + const tabId = index === 0 ? 'main' : file.type === 'survey' ? 'survey' : `file-${index}`; + return ( + <TabsTrigger key={tabId} value={tabId} className="text-xs"> + <div className="flex items-center space-x-1"> + {file.type === 'survey' ? <ClipboardList className="h-3 w-3" /> : <FileText className="h-3 w-3" />} + <span className="truncate">{file.name}</span> + {file.type === 'survey' && surveyData.completed && ( + <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">완료</Badge> + )} + </div> + </TabsTrigger> + ); + })} + </TabsList> + </div> + + <div className="flex-1 min-h-0 overflow-hidden relative"> + <div className={`absolute inset-0 p-3 ${activeTab === 'survey' ? 'block' : 'hidden'}`}> + <SurveyComponent /> + </div> + + <div className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`}> + {/* ✅ 수정: 스크롤 활성화 */} + <div className="w-full h-full overflow-auto"> + <div + ref={viewer} + className="w-full h-full min-h-[400px]" + style={{ + position: 'relative', + overflow: 'visible' + }} + > + {fileLoading && ( + <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> + <p className="text-sm text-muted-foreground">문서 로딩 중...</p> + </div> + )} + </div> + </div> + </div> + </div> + </Tabs> + ) : ( + // ✅ 수정: 다이얼로그에서 뷰어만 있는 경우도 동일한 구조 + <div className="h-full flex flex-col"> + <div className="flex-1 min-h-0 overflow-hidden relative"> + <div className="absolute inset-0"> + <div className="w-full h-full overflow-auto"> + <div + ref={viewer} + className="w-full h-full min-h-[400px]" + style={{ + position: 'relative', + overflow: 'visible' + }} + > + {fileLoading && ( + <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> + <p className="text-sm text-muted-foreground">문서 로딩 중...</p> + </div> + )} + </div> + </div> + </div> + </div> + </div> + )} + </div> + + <DialogFooter className="px-6 py-4 border-t bg-white flex-shrink-0"> + <Button variant="outline" onClick={handleClose} disabled={fileLoading}>취소</Button> + <Button onClick={handleSave} disabled={fileLoading || isAutoSignProcessing}> + <FileSignature className="h-4 w-4 mr-2" /> + 서명 완료 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> +); + // 다이얼로그 뷰어 렌더링 return ( <Dialog open={showDialog} onOpenChange={handleClose}> |
