// PDF 텍스트 패턴 기반 자동 서명 필드 생성 시스템 interface SignaturePattern { regex: RegExp; name: string; priority: number; offsetX?: number; // 텍스트로부터 X축 오프셋 offsetY?: number; // 텍스트로부터 Y축 오프셋 width?: number; // 서명 필드 너비 height?: number; // 서명 필드 높이 } interface DetectedSignatureLocation { pageIndex: number; text: string; rect: { x1: number; y1: number; x2: number; y2: number; }; pattern: SignaturePattern; confidence: number; } class AutoSignatureFieldDetector { private instance: WebViewerInstance; private signaturePatterns: SignaturePattern[]; constructor(instance: WebViewerInstance) { this.instance = instance; this.signaturePatterns = this.initializePatterns(); } private initializePatterns(): SignaturePattern[] { return [ // 한국어 패턴들 (우선순위 높음) { regex: /서명\s*[::]\s*[_\-\s]{3,}/gi, name: "한국어_서명_콜론", priority: 10, offsetX: 80, // "서명:" 텍스트 오른쪽으로 80px offsetY: -5, // 약간 위로 width: 150, height: 40 }, { regex: /서명란\s*[_\-\s]{0,}/gi, name: "한국어_서명란", priority: 9, offsetX: 60, offsetY: -5, width: 150, height: 40 }, { regex: /서명\s*[_\-\s]{5,}/gi, name: "한국어_서명_라인", priority: 8, offsetX: 50, offsetY: -5, width: 150, height: 40 }, { regex: /(계약자|갑|을)\s*서명\s*[::]?\s*[_\-\s]{0,}/gi, name: "한국어_계약자_서명", priority: 9, offsetX: 100, offsetY: -5, width: 150, height: 40 }, // 영어 패턴들 { regex: /signature\s*[::]\s*[_\-\s]{3,}/gi, name: "영어_signature_콜론", priority: 8, offsetX: 120, offsetY: -5, width: 150, height: 40 }, { regex: /sign\s+here\s*[::]?\s*[_\-\s]{0,}/gi, name: "영어_sign_here", priority: 9, offsetX: 100, offsetY: -5, width: 150, height: 40 }, { regex: /sign\s*[::]\s*[_\-\s]{3,}/gi, name: "영어_sign_콜론", priority: 7, offsetX: 60, offsetY: -5, width: 150, height: 40 }, // 날짜와 함께 나오는 패턴들 { regex: /날짜\s*[::]\s*[_\-\s]{3,}.*?서명\s*[::]\s*[_\-\s]{3,}/gi, name: "날짜_서명_조합", priority: 10, offsetX: 0, offsetY: -5, width: 150, height: 40 }, { regex: /date\s*[::]\s*[_\-\s]{3,}.*?signature\s*[::]\s*[_\-\s]{3,}/gi, name: "date_signature_조합", priority: 10, offsetX: 0, offsetY: -5, width: 150, height: 40 }, // 일반적인 양식 패턴들 { regex: /이름\s*[::]\s*[_\-\s]{5,}.*?서명\s*[::]\s*[_\-\s]{5,}/gi, name: "이름_서명_조합", priority: 8, offsetX: 0, offsetY: -5, width: 150, height: 40 }, { regex: /name\s*[::]\s*[_\-\s]{5,}.*?signature\s*[::]\s*[_\-\s]{5,}/gi, name: "name_signature_조합", priority: 8, offsetX: 0, offsetY: -5, width: 150, height: 40 } ]; } // 📄 메인 함수: 문서에서 서명 패턴 감지 및 필드 생성 async detectAndCreateSignatureFields(): Promise { console.log("🔍 자동 서명 필드 감지 시작..."); try { const { Core } = this.instance; const { documentViewer } = Core; await Core.PDFNet.initialize(); const doc = await documentViewer.getDocument().getPDFDoc(); // 1. 기존 서명 필드 확인 const existingFields = await this.getExistingSignatureFields(doc); console.log(`📊 기존 서명 필드: ${existingFields.length}개`); if (existingFields.length > 0) { console.log("✅ 기존 서명 필드가 있으므로 자동 생성 스킵"); return existingFields.map(f => f.name); } // 2. 텍스트 패턴 기반 서명 위치 감지 const detectedLocations = await this.detectSignatureLocations(doc); console.log(`🎯 감지된 서명 위치: ${detectedLocations.length}개`); // 3. 감지된 위치에 서명 필드 생성 const createdFields: string[] = []; for (const location of detectedLocations) { try { const fieldName = await this.createSignatureFieldAtLocation(doc, location); createdFields.push(fieldName); console.log(`✅ 서명 필드 생성: ${fieldName}`); } catch (error) { console.error(`📛 서명 필드 생성 실패:`, error); } } // 4. 문서 업데이트 if (createdFields.length > 0) { await documentViewer.refreshAll(); await documentViewer.updateView(); console.log(`🎉 총 ${createdFields.length}개 서명 필드 자동 생성 완료`); } else { console.warn("⚠️ 서명 패턴을 찾지 못했습니다. 기본 서명 필드 생성..."); const defaultField = await this.createDefaultSignatureField(doc); createdFields.push(defaultField); } return createdFields; } catch (error) { console.error("📛 자동 서명 필드 생성 실패:", error); return []; } } // 기존 서명 필드 확인 private async getExistingSignatureFields(doc: any): Promise { const { Core } = this.instance; const fields = []; try { const pageCount = await doc.getPageCount(); for (let i = 1; i <= pageCount; i++) { const page = await doc.getPage(i); const numAnnots = await page.getNumAnnots(); for (let j = 0; j < numAnnots; j++) { const annot = await page.getAnnot(j); const annotType = await annot.getType(); if (annotType === Core.PDFNet.Annot.Type.e_Widget) { const widget = await Core.PDFNet.WidgetAnnot.cast(annot); const field = await widget.getField(); const fieldType = await field.getType(); if (fieldType === Core.PDFNet.Field.Type.e_signature) { const fieldName = await field.getName(); fields.push({ name: fieldName, widget, page: i }); } } } } } catch (error) { console.warn("기존 필드 확인 중 에러:", error); } return fields; } // 텍스트 패턴 기반 서명 위치 감지 private async detectSignatureLocations(doc: any): Promise { const { Core } = this.instance; const detectedLocations: DetectedSignatureLocation[] = []; try { const pageCount = await doc.getPageCount(); for (let pageNum = 1; pageNum <= pageCount; pageNum++) { const page = await doc.getPage(pageNum); // 텍스트 추출 const textExtractor = await Core.PDFNet.TextExtractor.create(); await textExtractor.begin(page); // 텍스트와 위치 정보 추출 const wordList = []; let line = await textExtractor.getFirstLine(); while (line) { let word = await line.getFirstWord(); while (word) { const wordText = await word.getString(); const wordBox = await word.getBBox(); wordList.push({ text: wordText, x1: await wordBox.getX1(), y1: await wordBox.getY1(), x2: await wordBox.getX2(), y2: await wordBox.getY2() }); word = await word.getNext(); } line = await line.getNext(); } // 전체 페이지 텍스트 조합 const fullText = wordList.map(w => w.text).join(' '); // 패턴 매칭 for (const pattern of this.signaturePatterns) { const matches = Array.from(fullText.matchAll(pattern.regex)); for (const match of matches) { // 매치된 텍스트의 위치 찾기 const matchText = match[0]; const matchStart = match.index || 0; // 대략적인 위치 계산 (개선 가능) const location = this.calculateTextLocation(wordList, matchStart, matchText.length); if (location) { detectedLocations.push({ pageIndex: pageNum - 1, text: matchText, rect: { x1: location.x1 + (pattern.offsetX || 0), y1: location.y1 + (pattern.offsetY || 0), x2: location.x1 + (pattern.offsetX || 0) + (pattern.width || 150), y2: location.y1 + (pattern.offsetY || 0) + (pattern.height || 40) }, pattern: pattern, confidence: pattern.priority }); console.log(`🎯 패턴 매치: "${matchText}" (${pattern.name}) 페이지 ${pageNum}`); } } } } // 신뢰도 순으로 정렬 (중복 제거 포함) return this.deduplicateAndSort(detectedLocations); } catch (error) { console.error("텍스트 패턴 감지 실패:", error); return []; } } // 텍스트 위치 계산 (개선된 버전) private calculateTextLocation(wordList: any[], startIndex: number, length: number): any { if (wordList.length === 0) return null; // 간단한 구현: 첫 번째 단어의 위치 사용 // 실제로는 더 정교한 텍스트 매칭 필요 const totalChars = wordList.map(w => w.text).join(' ').length; const ratio = startIndex / totalChars; const targetWordIndex = Math.floor(ratio * wordList.length); const targetWord = wordList[Math.min(targetWordIndex, wordList.length - 1)]; return targetWord; } // 중복 제거 및 정렬 private deduplicateAndSort(locations: DetectedSignatureLocation[]): DetectedSignatureLocation[] { // 같은 페이지의 너무 가까운 위치들 제거 const filtered = locations.filter((loc, index) => { return !locations.slice(0, index).some(prevLoc => prevLoc.pageIndex === loc.pageIndex && Math.abs(prevLoc.rect.x1 - loc.rect.x1) < 100 && Math.abs(prevLoc.rect.y1 - loc.rect.y1) < 50 ); }); // 신뢰도(우선순위) 순으로 정렬 return filtered.sort((a, b) => b.confidence - a.confidence); } // 감지된 위치에 서명 필드 생성 private async createSignatureFieldAtLocation(doc: any, location: DetectedSignatureLocation): Promise { const { Core } = this.instance; const fieldName = `auto_signature_${location.pageIndex + 1}_${Date.now()}`; const page = await doc.getPage(location.pageIndex + 1); // 디지털 서명 필드 생성 const sigField = await doc.createDigitalSignatureField(fieldName); // 서명 위젯 생성 const rect = await Core.PDFNet.Rect.init( location.rect.x1, location.rect.y1, location.rect.x2, location.rect.y2 ); const widget = await Core.PDFNet.SignatureWidget.createWithDigitalSignatureField( doc, rect, sigField ); // 위젯 스타일 설정 await widget.setBackgroundColor( await Core.PDFNet.ColorPt.init(0.95, 0.95, 1.0), // 연한 파란색 3 // RGB ); await widget.setBorderColor( await Core.PDFNet.ColorPt.init(0.2, 0.4, 0.8), // 파란색 테두리 3 // RGB ); // 페이지에 위젯 추가 await page.annotPushBack(widget); console.log(`✅ 자동 서명 필드 생성: ${fieldName} (패턴: ${location.pattern.name})`); return fieldName; } // 기본 서명 필드 생성 (패턴을 찾지 못한 경우) private async createDefaultSignatureField(doc: any): Promise { const { Core } = this.instance; console.log("⚠️ 서명 패턴 미발견, 기본 위치에 서명 필드 생성"); const pageCount = await doc.getPageCount(); const lastPage = await doc.getPage(pageCount); const pageInfo = await lastPage.getPageInfo(); const pageWidth = await pageInfo.getWidth(); const pageHeight = await pageInfo.getHeight(); const fieldName = `default_signature_${Date.now()}`; const sigField = await doc.createDigitalSignatureField(fieldName); // 마지막 페이지 하단 중앙에 배치 const rect = await Core.PDFNet.Rect.init( pageWidth * 0.3, // 페이지 너비 30% 지점 pageHeight * 0.1, // 페이지 하단 10% 지점 pageWidth * 0.7, // 페이지 너비 70% 지점 pageHeight * 0.2 // 페이지 하단 20% 지점 ); const widget = await Core.PDFNet.SignatureWidget.createWithDigitalSignatureField( doc, rect, sigField ); await widget.setBackgroundColor( await Core.PDFNet.ColorPt.init(1.0, 0.95, 0.95), // 연한 핑크색 (주의 표시) 3 ); await widget.setBorderColor( await Core.PDFNet.ColorPt.init(0.8, 0.2, 0.2), // 빨간색 테두리 3 ); await lastPage.annotPushBack(widget); return fieldName; } } // ✅ BasicContractSignViewer에 통합할 수 있는 함수 export async function addAutoSignatureFieldsToDocument(instance: WebViewerInstance): Promise { if (!instance) { console.warn("⚠️ WebViewer 인스턴스가 없습니다."); return []; } try { const detector = new AutoSignatureFieldDetector(instance); const createdFields = await detector.detectAndCreateSignatureFields(); if (createdFields.length > 0) { console.log(`🎉 자동 서명 필드 생성 완료: ${createdFields.join(', ')}`); } return createdFields; } catch (error) { console.error("📛 자동 서명 필드 추가 실패:", error); return []; } } // ✅ 문서 로드 후 자동 호출되는 Hook export function useAutoSignatureFields(instance: WebViewerInstance | null) { const [signatureFields, setSignatureFields] = React.useState([]); const [isProcessing, setIsProcessing] = React.useState(false); React.useEffect(() => { if (!instance) return; const { documentViewer } = instance.Core; const handleDocumentLoaded = async () => { try { setIsProcessing(true); console.log("📄 문서 로드 완료, 자동 서명 필드 생성 시작..."); // 문서 로드 후 잠시 대기 (안정성을 위해) await new Promise(resolve => setTimeout(resolve, 1000)); const fields = await addAutoSignatureFieldsToDocument(instance); setSignatureFields(fields); } catch (error) { console.error("📛 자동 서명 필드 처리 실패:", error); } finally { setIsProcessing(false); } }; documentViewer.addEventListener('documentLoaded', handleDocumentLoaded); return () => { documentViewer.removeEventListener('documentLoaded', handleDocumentLoaded); }; }, [instance]); return { signatureFields, isProcessing, hasSignatureFields: signatureFields.length > 0 }; }