From 18954df6565108a469fb1608ea3715dd9bb1b02d Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 1 Sep 2025 09:12:09 +0000
Subject: (대표님) 구매 기본계약, gtc 개발
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../viewer/basic-contract-sign-viewer.tsx          | 256 ++++++++++++++-------
 1 file changed, 179 insertions(+), 77 deletions(-)

(limited to 'lib/basic-contract/viewer/basic-contract-sign-viewer.tsx')

diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
index 943878da..e52f0d79 100644
--- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
+++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
@@ -44,7 +44,12 @@ interface BasicContractSignViewerProps {
   setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>;
   onSurveyComplete?: () => void;
   onSignatureComplete?: () => void;
-  onGtcCommentStatusChange?: (hasComments: boolean, commentCount: number) => void;
+  onGtcCommentStatusChange?: (
+    hasComments: boolean,
+    commentCount: number,
+    reviewStatus?: string,
+    isComplete?: boolean
+  ) => void;
   mode?: 'vendor' | 'buyer'; // 추가된 mode prop
   t?: (key: string) => string;
 }
@@ -63,58 +68,15 @@ interface SignaturePattern {
 // 초간단 안전한 서명 필드 감지 클래스
 class AutoSignatureFieldDetector {
   private instance: WebViewerInstance;
-  private signaturePatterns: SignaturePattern[];
-  private mode: 'vendor' | 'buyer'; // mode 추가
+  private mode: 'vendor' | 'buyer';
 
   constructor(instance: WebViewerInstance, mode: 'vendor' | 'buyer' = 'vendor') {
     this.instance = instance;
     this.mode = mode;
-    this.signaturePatterns = this.initializePatterns();
-  }
-
-  private initializePatterns(): SignaturePattern[] {
-    return [
-      {
-        regex: /서명\s*[:：]\s*[_\-\s]{3,}/gi,
-        name: "한국어_서명_콜론",
-        priority: 10,
-        offsetX: 80,
-        offsetY: -5,
-        width: 150,
-        height: 40
-      },
-      {
-        regex: /서명란\s*[_\-\s]{0,}/gi,
-        name: "한국어_서명란",
-        priority: 9,
-        offsetX: 60,
-        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
-      }
-    ];
   }
 
   async detectAndCreateSignatureFields(): Promise<string[]> {
-    console.log(`🔍 안전한 서명 필드 감지 시작... (모드: ${this.mode})`);
+    console.log(`🔍 텍스트 기반 서명 필드 감지 시작... (모드: ${this.mode})`);
 
     try {
       if (!this.instance?.Core?.documentViewer) {
@@ -129,25 +91,122 @@ class AutoSignatureFieldDetector {
         throw new Error("PDF 문서가 로드되지 않았습니다.");
       }
 
-      console.log("📄 문서 확인 완료, 기본 서명 필드 생성...");
-      const defaultField = await this.createSimpleSignatureField();
+      // 모드에 따라 검색할 텍스트 결정
+      const searchText = this.mode === 'buyer'
+        ? '삼성중공업_서명란'
+        : '협력업체_서명란';
+
+      console.log(`📄 "${searchText}" 텍스트 검색 중...`);
 
-      console.log("✅ 서명 필드 생성 완료");
-      return [defaultField];
+      // 텍스트 검색 및 서명 필드 생성
+      const fieldName = await this.createSignatureFieldAtText(searchText);
+
+      if (fieldName) {
+        console.log(`✅ 텍스트 위치에 서명 필드 생성 완료: ${searchText}`);
+        return [fieldName];
+      } else {
+        // 텍스트를 찾지 못한 경우 기본 위치에 생성
+        console.log(`⚠️ "${searchText}" 텍스트를 찾지 못해 기본 위치에 생성`);
+        const defaultField = await this.createSimpleSignatureField();
+        return [defaultField];
+      }
 
     } catch (error) {
       console.error("📛 서명 필드 생성 실패:", error);
 
-      let errorMessage = "서명 필드 생성에 실패했습니다.";
-      if (error instanceof Error) {
-        if (error.message.includes("인스턴스")) {
-          errorMessage = "뷰어가 준비되지 않았습니다.";
-        } else if (error.message.includes("문서")) {
-          errorMessage = "문서를 불러오는 중입니다.";
+      // 오류 발생 시 기본 위치에 생성
+      try {
+        const defaultField = await this.createSimpleSignatureField();
+        return [defaultField];
+      } catch (fallbackError) {
+        throw new Error("서명 필드 생성에 실패했습니다.");
+      }
+    }
+  }
+
+  private async createSignatureFieldAtText(searchText: string): Promise<string | null> {
+    const { Core } = this.instance;
+    const { documentViewer, annotationManager } = Core;
+    const document = documentViewer.getDocument();
+
+    if (!document) return null;
+
+    try {
+      // 모든 페이지에서 텍스트 검색
+      const searchMode = Core.Search.Mode.PAGE_STOP | Core.Search.Mode.HIGHLIGHT;
+      const searchOptions = {
+        fullSearch: true,
+        onResult: null,
+      };
+
+      // 텍스트 검색 시작
+      const textSearchIterator = await document.getTextSearchIterator();
+      textSearchIterator.begin(searchText, searchMode);
+
+      let searchResult = await textSearchIterator.next();
+
+      // 검색 결과가 있는 경우
+      if (searchResult && searchResult.resultCode === Core.Search.ResultCode.FOUND) {
+        const pageNumber = searchResult.pageNum;
+        const quads = searchResult.quads;
+
+        if (quads && quads.length > 0) {
+          // 첫 번째 검색 결과의 위치 가져오기
+          const quad = quads[0];
+
+          // 쿼드의 좌표를 기반으로 서명 필드 위치 계산
+          const x = Math.min(quad.x1, quad.x2, quad.x3, quad.x4);
+          const y = Math.min(quad.y1, quad.y2, quad.y3, quad.y4);
+          const textWidth = Math.abs(quad.x2 - quad.x1);
+          const textHeight = Math.abs(quad.y3 - quad.y1);
+
+          // 서명 필드 생성
+          const fieldName = `signature_at_text_${Date.now()}`;
+          const flags = new Core.Annotations.WidgetFlags();
+          flags.set('Required', true);
+
+          const field = new Core.Annotations.Forms.Field(fieldName, {
+            type: 'Sig',
+            flags
+          });
+
+          const widget = new Core.Annotations.SignatureWidgetAnnotation(field, {
+            Width: 150,
+            Height: 50
+          });
+
+          widget.setPageNumber(pageNumber);
+
+          // 텍스트 바로 아래 또는 오른쪽에 서명 필드 배치
+          // 옵션 1: 텍스트 바로 아래
+          widget.setX(x);
+          widget.setY(y + textHeight + 5); // 텍스트 아래 5픽셀 간격
+
+          // 옵션 2: 텍스트 오른쪽 (필요시 아래 주석 해제)
+          // widget.setX(x + textWidth + 10); // 텍스트 오른쪽 10픽셀 간격
+          // widget.setY(y);
+
+          widget.setWidth(150);
+          widget.setHeight(50);
+
+          // 필드 매니저에 추가
+          const fm = annotationManager.getFieldManager();
+          fm.addField(field);
+          annotationManager.addAnnotation(widget);
+          annotationManager.drawAnnotationsFromList([widget]);
+
+          console.log(`📌 서명 필드를 페이지 ${pageNumber}의 "${searchText}" 위치에 생성`);
+
+          return fieldName;
         }
       }
 
-      throw new Error(errorMessage);
+      console.log(`⚠️ "${searchText}" 텍스트를 찾을 수 없음`);
+      return null;
+
+    } catch (error) {
+      console.error(`📛 텍스트 검색 중 오류: ${error}`);
+      return null;
     }
   }
 
@@ -163,11 +222,18 @@ class AutoSignatureFieldDetector {
     const flags = new Annotations.WidgetFlags();
     flags.set('Required', true);
 
-    const field = new Core.Annotations.Forms.Field(fieldName, { type: 'Sig', flags });
+    const field = new Core.Annotations.Forms.Field(fieldName, {
+      type: 'Sig',
+      flags
+    });
+
+    const widget = new Annotations.SignatureWidgetAnnotation(field, {
+      Width: 150,
+      Height: 50
+    });
 
-    const widget = new Annotations.SignatureWidgetAnnotation(field, { Width: 150, Height: 50 });
     widget.setPageNumber(page);
-    
+
     // 구매자 모드일 때는 왼쪽 하단으로 위치 설정
     if (this.mode === 'buyer') {
       widget.setX(w * 0.1); // 왼쪽 (10%)
@@ -177,7 +243,7 @@ class AutoSignatureFieldDetector {
       widget.setX(w * 0.7); // 오른쪽 (70%)
       widget.setY(h * 0.85); // 하단 (85%)
     }
-    
+
     widget.setWidth(150);
     widget.setHeight(50);
 
@@ -190,6 +256,7 @@ class AutoSignatureFieldDetector {
   }
 }
 
+
 function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendor' | 'buyer' = 'vendor') {
   const [signatureFields, setSignatureFields] = useState<string[]>([]);
   const [isProcessing, setIsProcessing] = useState(false);
@@ -280,15 +347,23 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
           }
 
           if (fields.length > 0) {
+            const hasTextBasedField = fields.some(field => field.startsWith('signature_at_text_'));
             const hasSimpleField = fields.some(field => field.startsWith('simple_signature_'));
 
-            if (hasSimpleField) {
-              const positionMessage = mode === 'buyer' 
+            if (hasTextBasedField) {
+              const searchText = mode === 'buyer' ? '삼성중공업_서명란' : '협력업체_서명란';
+              toast.success(`📝 "${searchText}" 위치에 서명 필드가 생성되었습니다.`, {
+                description: "해당 텍스트 근처의 파란색 영역에서 서명해주세요.",
+                icon: <FileSignature className="h-4 w-4 text-blue-500" />,
+                duration: 5000
+              });
+            } else if (hasSimpleField) {
+              const positionMessage = mode === 'buyer'
                 ? "마지막 페이지 왼쪽 하단의 파란색 영역에서 서명해주세요."
                 : "마지막 페이지 하단의 파란색 영역에서 서명해주세요.";
 
-              toast.success("📝 서명 필드가 생성되었습니다.", {
-                description: positionMessage,
+              toast.info("📝 기본 위치에 서명 필드가 생성되었습니다.", {
+                description: `검색 텍스트를 찾을 수 없어 ${positionMessage}`,
                 icon: <FileSignature className="h-4 w-4 text-blue-500" />,
                 duration: 5000
               });
@@ -508,7 +583,16 @@ export function BasicContractSignViewer({
   const [isInitialLoaded, setIsInitialLoaded] = useState<boolean>(false);
   const [surveyTemplate, setSurveyTemplate] = useState<SurveyTemplateWithQuestions | null>(null);
   const [surveyLoading, setSurveyLoading] = useState<boolean>(false);
-  const [gtcCommentStatus, setGtcCommentStatus] = useState<{ hasComments: boolean; commentCount: number }>({ hasComments: false, commentCount: 0 });
+  const [gtcCommentStatus, setGtcCommentStatus] = useState<{
+    hasComments: boolean;
+    commentCount: number;
+    reviewStatus?: string;
+    isComplete?: boolean;
+  }>({
+    hasComments: false,
+    commentCount: 0,
+    isComplete: false
+  });
 
   console.log(surveyTemplate, "surveyTemplate")
 
@@ -739,9 +823,12 @@ export function BasicContractSignViewer({
                     stamp.Width = Width;
                     stamp.Height = Height;
 
-                    await stamp.setImageData(signatureImage.data.dataUrl);
-                    annot.sign(stamp);
-                    annot.setFieldFlag(WidgetFlags.READ_ONLY, true);
+                    if (signatureImage) {
+                      await stamp.setImageData(signatureImage.data.dataUrl);
+                      annot.sign(stamp);
+                      annot.setFieldFlag(WidgetFlags.READ_ONLY, true);
+                    }
+
                   }
                 }
               });
@@ -994,8 +1081,8 @@ export function BasicContractSignViewer({
           return;
         }
 
-        if (isGTCTemplate && gtcCommentStatus.hasComments) {
-          toast.error("GTC 조항에 코멘트가 있어 서명할 수 없습니다.");
+        if (isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete) {
+          toast.error("GTC 조항에 미해결 코멘트가 있어 서명할 수 없습니다.");
           toast.info("모든 코멘트를 삭제하거나 협의를 완료한 후 서명해주세요.");
           setActiveTab('clauses');
           return;
@@ -1130,9 +1217,9 @@ export function BasicContractSignViewer({
                 {/* GTC 조항 컴포넌트 */}
                 <GtcClausesComponent
                   contractId={contractId}
-                  onCommentStatusChange={(hasComments, commentCount) => {
-                    setGtcCommentStatus({ hasComments, commentCount });
-                    onGtcCommentStatusChange?.(hasComments, commentCount);
+                  onCommentStatusChange={(hasComments, commentCount, reviewStatus, isComplete) => {
+                    setGtcCommentStatus({ hasComments, commentCount, reviewStatus, isComplete });
+                    onGtcCommentStatusChange?.(hasComments, commentCount, reviewStatus, isComplete);
                   }}
                   t={t}
                 />
@@ -1193,9 +1280,14 @@ export function BasicContractSignViewer({
     );
   }
 
-  const handleGtcCommentStatusChange = React.useCallback((hasComments: boolean, commentCount: number) => {
-    setGtcCommentStatus({ hasComments, commentCount });
-    onGtcCommentStatusChange?.(hasComments, commentCount);
+  const handleGtcCommentStatusChange = React.useCallback((
+    hasComments: boolean,
+    commentCount: number,
+    reviewStatus?: string,
+    isComplete?: boolean
+  ) => {
+    setGtcCommentStatus({ hasComments, commentCount, reviewStatus, isComplete });
+    onGtcCommentStatusChange?.(hasComments, commentCount, reviewStatus, isComplete);
   }, [onGtcCommentStatusChange]);
 
   // 다이얼로그 뷰어 렌더링
@@ -1230,6 +1322,16 @@ export function BasicContractSignViewer({
                 ⚠️ GTC 조항에 {gtcCommentStatus.commentCount}개의 코멘트가 있어 서명할 수 없습니다.
               </span>
             )}
+            {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete && (
+              <span className="block mt-1 text-red-600">
+                ⚠️ GTC 조항에 {gtcCommentStatus.commentCount}개의 미해결 코멘트가 있어 서명할 수 없습니다.
+              </span>
+            )}
+            {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && gtcCommentStatus.isComplete && (
+              <span className="block mt-1 text-green-600">
+                ✅ GTC 조항 협의가 완료되어 서명 가능합니다.
+              </span>
+            )}
           </DialogDescription>
         </DialogHeader>
 
-- 
cgit v1.2.3

