From b0fe980376fcf1a19ff4b90851ca8b01f378fdc0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 07:30:30 +0000 Subject: (임수민) 기본계약서 자동서명 다중 입력 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../viewer/basic-contract-sign-viewer.tsx | 202 ++++++++++++--------- 1 file changed, 116 insertions(+), 86 deletions(-) diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx index d1492fdb..eeebae4e 100644 --- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx +++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx @@ -114,12 +114,12 @@ class AutoSignatureFieldDetector { console.log(`📄 "${searchText}" 텍스트 검색 중...`); - // 텍스트 검색 및 서명 필드 생성 - const fieldName = await this.createSignatureFieldAtText(searchText); + // 텍스트 검색 및 서명 필드 생성 (모든 매치 찾기) + const fieldNames = await this.createSignatureFieldAtText(searchText); - if (fieldName) { - console.log(`✅ 텍스트 위치에 서명 필드 생성 완료: ${searchText}`); - return [fieldName]; + if (fieldNames.length > 0) { + console.log(`✅ ${fieldNames.length}개의 "${searchText}" 위치에 서명 필드 생성 완료`); + return fieldNames; } else { // 텍스트를 찾지 못한 경우 기본 위치에 생성 console.log(`⚠️ "${searchText}" 텍스트를 찾지 못해 기본 위치에 생성`); @@ -140,74 +140,88 @@ class AutoSignatureFieldDetector { } } - private async createSignatureFieldAtText(searchText: string): Promise { + private async createSignatureFieldAtText(searchText: string): Promise { const { Core } = this.instance; const { documentViewer, annotationManager, Annotations } = Core; const doc = documentViewer.getDocument(); - if (!doc) return null; + if (!doc) return []; try { const pageCount = documentViewer.getPageCount(); + const fieldNames: string[] = []; + // 모든 페이지에서 모든 매치 찾기 for (let pageNumber = 1; pageNumber <= pageCount; pageNumber++) { // 1) 페이지 텍스트 로드 const pageText = await doc.loadPageText(pageNumber); + if (!pageText) continue; - // 2) 해당 페이지에서 검색어 위치 찾기 (첫 매치만 사용) - const startIndex = pageText.indexOf(searchText); - if (startIndex === -1) continue; + // 2) 해당 페이지에서 모든 검색어 위치 찾기 + let searchIndex = 0; + while (true) { + const startIndex = pageText.indexOf(searchText, searchIndex); + if (startIndex === -1) break; // 더 이상 찾을 수 없으면 다음 페이지로 - const endIndex = startIndex + searchText.length; + const endIndex = startIndex + searchText.length; - // 3) 검색어의 문자 단위 Quads 얻기 - const quads = await doc.getTextPosition(pageNumber, startIndex, endIndex); - if (!quads || quads.length === 0) continue; - - // 첫 글자의 quad만 사용해 대략적인 위치 산출 - const q = quads[0] as any; // PDFTron의 Quad 타입 - const x = Math.min(q.x1, q.x2, q.x3, q.x4); - const y = Math.min(q.y1, q.y2, q.y3, q.y4); - const textHeight = Math.abs(q.y3 - q.y1); - - // 4) 서명 필드 생성 - const fieldName = `signature_at_text_${Date.now()}`; - const signatureY = y + textHeight + 5; - - // 위치 정보 저장 - this.location = { - pageNumber, - x, - y: signatureY, - fieldName, - searchText - }; - - const flags = new Annotations.WidgetFlags(); - flags.set('Required', true); - - const field = new Core.Annotations.Forms.Field(fieldName, { type: 'Sig', flags }); - const widget = new Annotations.SignatureWidgetAnnotation(field, { Width: 150, Height: 50 }); - - widget.setPageNumber(pageNumber); - // 텍스트 바로 아래에 배치 (필요하면 오른쪽 배치로 바꿀 수 있음) - widget.setX(x); - widget.setY(signatureY); - widget.setWidth(150); - widget.setHeight(50); + // 3) 검색어의 문자 단위 Quads 얻기 + const quads = await doc.getTextPosition(pageNumber, startIndex, endIndex); + if (!quads || quads.length === 0) { + searchIndex = startIndex + 1; + continue; + } - const fm = annotationManager.getFieldManager(); - fm.addField(field); - annotationManager.addAnnotation(widget); - annotationManager.drawAnnotationsFromList([widget]); + // 첫 글자의 quad만 사용해 대략적인 위치 산출 + const q = quads[0] as any; // PDFTron의 Quad 타입 + const x = Math.min(q.x1, q.x2, q.x3, q.x4); + const y = Math.min(q.y1, q.y2, q.y3, q.y4); + const textHeight = Math.abs(q.y3 - q.y1); - return fieldName; + // 4) 서명 필드 생성 + const fieldName = `signature_at_text_${Date.now()}_${fieldNames.length}`; + const signatureY = y + textHeight + 5; + + // 첫 번째 필드의 위치 정보만 저장 (서명란으로 이동 기능용) + if (fieldNames.length === 0) { + this.location = { + pageNumber, + x, + y: signatureY, + fieldName, + searchText + }; + } + + const flags = new Annotations.WidgetFlags(); + flags.set('Required', true); + + const field = new Core.Annotations.Forms.Field(fieldName, { type: 'Sig', flags }); + const widget = new Annotations.SignatureWidgetAnnotation(field, { Width: 150, Height: 50 }); + + widget.setPageNumber(pageNumber); + // 텍스트 바로 아래에 배치 + widget.setX(x); + widget.setY(signatureY); + widget.setWidth(150); + widget.setHeight(50); + + const fm = annotationManager.getFieldManager(); + fm.addField(field); + annotationManager.addAnnotation(widget); + annotationManager.drawAnnotationsFromList([widget]); + + fieldNames.push(fieldName); + console.log(`✅ 페이지 ${pageNumber}의 "${searchText}" 위치에 서명 필드 생성 (${fieldNames.length}번째)`); + + // 다음 검색을 위해 인덱스 이동 + searchIndex = startIndex + 1; + } } - // 모든 페이지에서 못 찾으면 null - return null; + return fieldNames; } catch (e) { console.error('텍스트 기반 서명 필드 생성 중 오류', e); - return null; + return []; } } @@ -269,56 +283,72 @@ const applyBuyerSignatureAutomatically = async (instance: WebViewerInstance) => if (!doc) return; try { - console.log('🔍 구매자 자동 서명: "삼성중공업_서명란"만 검색'); + console.log('🔍 구매자 자동 서명: "삼성중공업_서명란" 전체 검색'); const TARGET = '삼성중공업_서명란'; // ✅ 정확히 이 문자열만 허용 const pageCount = documentViewer.getPageCount(); + let signatureCount = 0; + const buyerSignature = await getBuyerSignatureFileWithFallback(); // 기존 프로젝트 함수 그대로 사용 + if (!buyerSignature?.data?.dataUrl) { + console.warn('⚠️ 구매자 서명 이미지가 없습니다.'); + return; + } + + // 모든 페이지에서 모든 "삼성중공업_서명란" 찾기 for (let pageNumber = 1; pageNumber <= pageCount; pageNumber++) { const pageText = await doc.loadPageText(pageNumber); if (!pageText) continue; - const startIndex = pageText.indexOf(TARGET); - if (startIndex === -1) continue; + // 한 페이지에서 여러 개의 매치를 찾기 위해 반복 검색 + let searchIndex = 0; + while (true) { + const startIndex = pageText.indexOf(TARGET, searchIndex); + if (startIndex === -1) break; // 더 이상 찾을 수 없으면 다음 페이지로 - const endIndex = startIndex + TARGET.length; + const endIndex = startIndex + TARGET.length; - // 문자 쿼드 → 바운딩 박스 - const quads = await doc.getTextPosition(pageNumber, startIndex, endIndex); - if (!quads?.length) continue; + // 문자 쿼드 → 바운딩 박스 + const quads = await doc.getTextPosition(pageNumber, startIndex, endIndex); + if (!quads?.length) { + searchIndex = startIndex + 1; + continue; + } - const xs: number[] = [], ys: number[] = []; - quads.forEach(q => { xs.push(q.x1, q.x2, q.x3, q.x4); ys.push(q.y1, q.y2, q.y3, q.y4); }); - const minX = Math.min(...xs), maxY = Math.max(...ys); + const xs: number[] = [], ys: number[] = []; + quads.forEach((q: any) => { xs.push(q.x1, q.x2, q.x3, q.x4); ys.push(q.y1, q.y2, q.y3, q.y4); }); + const minX = Math.min(...xs), maxY = Math.max(...ys); - // 텍스트 바로 아래에 스탬프(서명 이미지) 배치 - const widgetX = minX; - const widgetY = maxY + 5; - const widgetW = 150; - const widgetH = 50; + // 텍스트 바로 아래에 스탬프(서명 이미지) 배치 + const widgetX = minX; + const widgetY = maxY + 5; + const widgetW = 150; + const widgetH = 50; - const buyerSignature = await getBuyerSignatureFileWithFallback(); // 기존 프로젝트 함수 그대로 사용 - if (!buyerSignature?.data?.dataUrl) { - console.warn('⚠️ 구매자 서명 이미지가 없습니다.'); - return; - } + const stamp = new Annotations.StampAnnotation(); + stamp.PageNumber = pageNumber; + stamp.X = widgetX; + stamp.Y = widgetY; + stamp.Width = widgetW; + stamp.Height = widgetH; + await stamp.setImageData(buyerSignature.data.dataUrl); - const stamp = new Annotations.StampAnnotation(); - stamp.PageNumber = pageNumber; - stamp.X = widgetX; - stamp.Y = widgetY; - stamp.Width = widgetW; - stamp.Height = widgetH; - await stamp.setImageData(buyerSignature.data.dataUrl); + annotationManager.addAnnotation(stamp); + annotationManager.drawAnnotationsFromList([stamp]); - annotationManager.addAnnotation(stamp); - annotationManager.drawAnnotationsFromList([stamp]); + signatureCount++; + console.log(`✅ 페이지 ${pageNumber}의 "삼성중공업_서명란" 위치에 자동 서명 적용 완료 (${signatureCount}번째)`); - console.log('✅ "삼성중공업_서명란" 위치에 자동 서명 적용 완료'); - return; // 첫 매치만 처리 + // 다음 검색을 위해 인덱스 이동 + searchIndex = startIndex + 1; + } } - console.warn('⚠️ 문서에서 "삼성중공업_서명란"을 찾지 못했습니다.'); + if (signatureCount > 0) { + console.log(`✅ 총 ${signatureCount}개의 "삼성중공업_서명란" 위치에 자동 서명 적용 완료`); + } else { + console.warn('⚠️ 문서에서 "삼성중공업_서명란"을 찾지 못했습니다.'); + } } catch (error) { console.error('구매자 자동 서명 처리 실패:', error); } -- cgit v1.2.3