summaryrefslogtreecommitdiff
path: root/lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx')
-rw-r--r--lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx493
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