diff options
Diffstat (limited to 'lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx')
| -rw-r--r-- | lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx | 493 |
1 files changed, 493 insertions, 0 deletions
diff --git a/lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx b/lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx new file mode 100644 index 00000000..7de8062c --- /dev/null +++ b/lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx @@ -0,0 +1,493 @@ +// 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<string[]> { + 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<any[]> { + 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<DetectedSignatureLocation[]> { + 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<string> { + 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<string> { + 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<string[]> { + 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<string[]>([]); + 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 + }; + }
\ No newline at end of file |
