From d19cca70ad1689807192a8784efc3091bf677816 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 17 Nov 2025 08:26:41 +0000
Subject: (임수민) 서명란 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../viewer/basic-contract-sign-viewer.tsx          | 247 +++++++++++----------
 1 file changed, 127 insertions(+), 120 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 eeebae4e..4d98675b 100644
--- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
+++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
@@ -9,7 +9,7 @@ import React, {
 } from "react";
 import { WebViewerInstance } from "@pdftron/webviewer";
 import { Loader2, FileText, ClipboardList, AlertTriangle, FileSignature, Target, CheckCircle2, BookOpen } from "lucide-react";
-import { toast } from "sonner";
+import { useToast } from "@/hooks/use-toast";
 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
@@ -24,7 +24,7 @@ import {
 import { getActiveSurveyTemplate, getVendorSignatureFile, type SurveyTemplateWithQuestions } from '../service';
 import { useConditionalSurvey } from '../vendor-table/survey-conditional';
 import { SurveyComponent } from './SurveyComponent';
-import { GtcClausesComponent } from './GtcClausesComponent';
+import { AgreementCommentList } from '../agreement-comments/agreement-comment-list';
 import { getBuyerSignatureFileWithFallback } from "@/lib/shi-signature/buyer-signature";
 
 interface FileInfo {
@@ -181,7 +181,7 @@ class AutoSignatureFieldDetector {
           const fieldName = `signature_at_text_${Date.now()}_${fieldNames.length}`;
           const signatureY = y + textHeight + 5;
           
-          // 첫 번째 필드의 위치 정보만 저장 (서명란으로 이동 기능용)
+          // 첫 번째 필드의 위치 정보만 저장
           if (fieldNames.length === 0) {
             this.location = {
               pageNumber,
@@ -355,7 +355,11 @@ const applyBuyerSignatureAutomatically = async (instance: WebViewerInstance) =>
 };
 
 
-function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendor' | 'buyer' = 'vendor') {
+function useAutoSignatureFields(
+  instance: WebViewerInstance | null, 
+  mode: 'vendor' | 'buyer' = 'vendor',
+  toastFn?: ReturnType<typeof useToast>['toast']
+) {
   const [signatureFields, setSignatureFields] = useState<string[]>([]);
   const [isProcessing, setIsProcessing] = useState(false);
   const [error, setError] = useState<string | null>(null);
@@ -431,7 +435,11 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
               processedDocumentRef.current = documentId;
             }
 
-            toast.success(`📋 ${fieldNames.length}개의 기존 서명 필드를 확인했습니다.`);
+            if (toastFn) {
+              toastFn({
+                title: `📋 ${fieldNames.length}개의 기존 서명 필드를 확인했습니다.`
+              });
+            }
             return;
           }
 
@@ -455,23 +463,23 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
             const hasTextBasedField = fields.some(field => field.startsWith('signature_at_text_'));
             const hasSimpleField = fields.some(field => field.startsWith('simple_signature_'));
 
-            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.info("📝 기본 위치에 서명 필드가 생성되었습니다.", {
-                description: `검색 텍스트를 찾을 수 없어 ${positionMessage}`,
-                icon: <FileSignature className="h-4 w-4 text-blue-500" />,
-                duration: 5000
-              });
+            if (toastFn) {
+              if (hasTextBasedField) {
+                const searchText = mode === 'buyer' ? '삼성중공업_서명란' : '협력업체_서명란';
+                toastFn({
+                  title: `📝 "${searchText}" 위치에 서명 필드가 생성되었습니다.`,
+                  description: "해당 텍스트 근처의 파란색 영역에서 서명해주세요."
+                });
+              } else if (hasSimpleField) {
+                const positionMessage = mode === 'buyer'
+                  ? "마지막 페이지 왼쪽 하단의 파란색 영역에서 서명해주세요."
+                  : "마지막 페이지 하단의 파란색 영역에서 서명해주세요.";
+
+                toastFn({
+                  title: "📝 기본 위치에 서명 필드가 생성되었습니다.",
+                  description: `검색 텍스트를 찾을 수 없어 ${positionMessage}`
+                });
+              }
             }
           }
 
@@ -480,10 +488,12 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
           const errorMessage = error instanceof Error ? error.message : "서명 필드 처리에 실패했습니다.";
           setError(errorMessage);
 
-          toast.info("수동 서명 모드", {
-            description: "문서에서 서명할 위치를 직접 클릭해주세요.",
-            icon: <FileSignature className="h-4 w-4 text-blue-500" />
-          });
+          if (toastFn) {
+            toastFn({
+              title: "수동 서명 모드",
+              description: "문서에서 서명할 위치를 직접 클릭해주세요."
+            });
+          }
         } finally {
           setIsProcessing(false);
           processingRef.current = false;
@@ -528,7 +538,7 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
       processedDocumentRef.current = null;
       handlerRef.current = null;
     };
-  }, [instance, mode]); // mode 의존성 추가
+  }, [instance, mode, toastFn]); // mode, toastFn 의존성 추가
 
   // 컴포넌트 언마운트 시 정리
   useEffect(() => {
@@ -550,53 +560,6 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
   };
 }
 
-// 서명란으로 이동하는 함수 추가
-const navigateToSignatureField = (
-  instance: WebViewerInstance | null,
-  location: SignatureFieldLocation | null
-) => {
-  if (!instance || !location) {
-    toast.error("서명란 위치를 찾을 수 없습니다.");
-    return;
-  }
-
-  try {
-    const { documentViewer, annotationManager } = instance.Core;
-    
-    // 1. 해당 페이지로 이동
-    documentViewer.setCurrentPage(location.pageNumber);
-    
-    // 2. 서명 필드 위젯 찾기
-    const annotations = annotationManager.getAnnotationsList();
-    const signatureWidget = annotations.find(
-      (annot: any) => annot instanceof instance.Core.Annotations.SignatureWidgetAnnotation &&
-      annot.getField()?.name === location.fieldName
-    );
-
-    if (signatureWidget) {
-      // 위젯이 있으면 선택하여 자동으로 해당 위치로 스크롤
-      annotationManager.selectAnnotation(signatureWidget);
-      
-      // 추가로 스크롤 위치 조정
-      setTimeout(() => {
-        try {
-          // annotation을 다시 선택하여 스크롤 보장
-          annotationManager.selectAnnotation(signatureWidget);
-        } catch (scrollError) {
-          console.warn("스크롤 조정 실패:", scrollError);
-        }
-      }, 300);
-      
-      toast.success(`서명란으로 이동했습니다. (페이지 ${location.pageNumber})`);
-    } else {
-      // 위젯을 찾지 못한 경우 페이지만 이동
-      toast.info(`페이지 ${location.pageNumber}로 이동했습니다.`);
-    }
-  } catch (error) {
-    console.error("서명란으로 이동 실패:", error);
-    toast.error("서명란으로 이동하는데 실패했습니다.");
-  }
-};
 // XFDF 기반 서명 감지
 function useSignatureDetection(instance: WebViewerInstance | null, onSignatureComplete?: () => void) {
   const [hasValidSignature, setHasValidSignature] = useState(false);
@@ -730,6 +693,7 @@ export function BasicContractSignViewer({
   mode = 'vendor', // 기본값 vendor
   t = (key: string) => key,
 }: BasicContractSignViewerProps) {
+  const { toast } = useToast();
 
   const [fileLoading, setFileLoading] = useState<boolean>(true);
   const [activeTab, setActiveTab] = useState<string>("main");
@@ -768,7 +732,7 @@ export function BasicContractSignViewer({
     hasSignatureFields, 
     error: autoSignError,
     signatureLocation // 위치 정보 추가
-  } = useAutoSignatureFields(webViewerInstance.current || instance, mode);
+  } = useAutoSignatureFields(webViewerInstance.current || instance, mode, toast);
 
   const { hasValidSignature } = useSignatureDetection(webViewerInstance.current || instance, onSignatureComplete);
 
@@ -788,7 +752,7 @@ export function BasicContractSignViewer({
       });
     }
 
-    // 구매자 모드일 때는 추가 파일, 설문조사, 조항 검토 탭 제외
+    // 구매자 모드일 때는 추가 파일, 설문조사, 협의 코멘트 탭 제외
     if (mode === 'buyer') {
       return files; // 메인 계약서 파일만 반환
     }
@@ -814,7 +778,7 @@ export function BasicContractSignViewer({
     if (isGTCTemplate) {
       files.push({
         path: "",
-        name: "조항 검토",
+        name: "협의 코멘트",
         type: "clauses",
       });
     }
@@ -1056,19 +1020,28 @@ export function BasicContractSignViewer({
 
               if (showToast) {
                 setFileLoading(false);
-                toast.error(errorMessage);
+                toast({
+                  title: errorMessage,
+                  variant: "destructive"
+                });
               }
             });
 
           }).catch((error) => {
             console.error("📛 WebViewer 초기화 실패:", error);
             setFileLoading(false);
-            toast.error("뷰어 초기화에 실패했습니다.");
+            toast({
+              title: "뷰어 초기화에 실패했습니다.",
+              variant: "destructive"
+            });
           });
         }).catch((error) => {
           console.error("📛 WebViewer 모듈 로드 실패:", error);
           setFileLoading(false);
-          toast.error("뷰어 모듈을 불러오는데 실패했습니다.");
+          toast({
+            title: "뷰어 모듈을 불러오는데 실패했습니다.",
+            variant: "destructive"
+          });
         });
       };
 
@@ -1139,7 +1112,10 @@ export function BasicContractSignViewer({
           msg = "";
         }
       }
-      if (msg) toast.error(msg);
+      if (msg) toast({
+        title: msg,
+        variant: "destructive"
+      });
     } finally {
       setFileLoading(false);
     }
@@ -1250,10 +1226,13 @@ export function BasicContractSignViewer({
       const { documentViewer, annotationManager } = currentInstance.Core;
       const doc = documentViewer.getDocument();
 
-      if (!doc) {
-        toast.error("문서가 로드되지 않았습니다.");
-        return;
-      }
+        if (!doc) {
+          toast({
+            title: "문서가 로드되지 않았습니다.",
+            variant: "destructive"
+          });
+          return;
+        }
 
       const formData = await collectFormData(currentInstance);
 
@@ -1266,14 +1245,20 @@ export function BasicContractSignViewer({
       // 구매자 모드일 때는 설문조사와 GTC 검증 건너뛰기
       if (mode !== 'buyer') {
         if (isComplianceTemplate && !surveyData.completed) {
-          toast.error("준법 설문조사를 먼저 완료해주세요.");
+          toast({
+            title: "준법 설문조사를 먼저 완료해주세요.",
+            variant: "destructive"
+          });
           setActiveTab('survey');
           return;
         }
 
         if (isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete) {
-          toast.error("GTC 조항에 미해결 코멘트가 있어 서명할 수 없습니다.");
-          toast.info("모든 코멘트를 삭제하거나 협의를 완료한 후 서명해주세요.");
+          toast({
+            title: "미해결 코멘트가 있어 서명할 수 없습니다.",
+            description: "모든 코멘트를 삭제하거나 협의를 완료한 후 서명해주세요.",
+            variant: "destructive"
+          });
           setActiveTab('clauses');
           return;
         }
@@ -1283,14 +1268,19 @@ export function BasicContractSignViewer({
         await onSign(documentData, { formData, surveyData, signatureFields });
       } else {
         const actionText = mode === 'buyer' ? "최종승인" : "서명";
-        toast.success(`계약서가 성공적으로 ${actionText}되었습니다.`);
+        toast({
+          title: `계약서가 성공적으로 ${actionText}되었습니다.`
+        });
       }
 
       handleClose();
     } catch (error) {
       console.error(`📛 ${mode === 'buyer' ? '최종승인' : '서명'} 저장 실패:`, error);
       const actionText = mode === 'buyer' ? "최종승인" : "서명";
-      toast.error(`${actionText}을 저장하는데 실패했습니다.`);
+      toast({
+        title: `${actionText}을 저장하는데 실패했습니다.`,
+        variant: "destructive"
+      });
     }
   };
 
@@ -1326,17 +1316,6 @@ export function BasicContractSignViewer({
               <Target className="h-3 w-3 mr-1" />
               {signatureFields.length}개 서명 필드 자동 생성됨 {mode === 'buyer' ? '(왼쪽 하단)' : ''}
             </Badge>
-            {signatureLocation && (
-              <Button
-                variant="ghost"
-                size="sm"
-                className="h-6 px-2 text-xs hover:bg-blue-50"
-                onClick={() => navigateToSignatureField(currentInstance, signatureLocation)}
-              >
-                <Target className="h-3 w-3 mr-1" />
-                서명란으로 이동
-              </Button>
-            )}
           </>
         ) : null}
 
@@ -1420,15 +1399,27 @@ export function BasicContractSignViewer({
               <div
                 className={`absolute inset-0 p-3 ${activeTab === 'clauses' ? 'block' : 'hidden'}`}
               >
-                {/* GTC 조항 컴포넌트 */}
-                <GtcClausesComponent
-                  contractId={contractId}
-                  onCommentStatusChange={(hasComments, commentCount, reviewStatus, isComplete) => {
-                    setGtcCommentStatus({ hasComments, commentCount, reviewStatus, isComplete });
-                    onGtcCommentStatusChange?.(hasComments, commentCount, reviewStatus, isComplete);
-                  }}
-                  t={t}
-                />
+                {/* 새로운 협의 코멘트 리스트 */}
+                {contractId ? (
+                  <div className="h-full">
+                    <AgreementCommentList
+                      basicContractId={contractId}
+                      currentUserType={mode === 'vendor' ? 'Vendor' : 'SHI'}
+                      readOnly={false}
+                      onCommentCountChange={(count) => {
+                        const hasComments = count > 0;
+                        const reviewStatus = hasComments ? 'negotiating' : 'draft';
+                        const isComplete = false;
+                        setGtcCommentStatus({ hasComments, commentCount: count, reviewStatus, isComplete });
+                        onGtcCommentStatusChange?.(hasComments, count, reviewStatus, isComplete);
+                      }}
+                    />
+                  </div>
+                ) : (
+                  <div className="flex items-center justify-center h-full text-gray-500">
+                    <p>계약서 정보를 불러올 수 없습니다. (contractId: {String(contractId)})</p>
+                  </div>
+                )}
               </div>
 
               <div
@@ -1511,7 +1502,7 @@ export function BasicContractSignViewer({
               <span className="block mt-1 text-amber-600">📋 준법 설문조사를 먼저 완료해주세요.</span>
             )}
             {mode !== 'buyer' && isGTCTemplate && (
-              <span className="block mt-1 text-blue-600">📋 GTC 조항을 검토하고 코멘트가 없는지 확인해주세요.</span>
+              <span className="block mt-1 text-blue-600">📋 협의 코멘트를 확인하고 모든 협의가 완료되었는지 확인해주세요.</span>
             )}
             {hasSignatureFields && (
               <span className="block mt-1 text-green-600">
@@ -1525,17 +1516,17 @@ export function BasicContractSignViewer({
             )}
             {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && (
               <span className="block mt-1 text-red-600">
-                ⚠️ GTC 조항에 {gtcCommentStatus.commentCount}개의 코멘트가 있어 서명할 수 없습니다.
+                ⚠️ {gtcCommentStatus.commentCount}개의 코멘트가 있어 서명할 수 없습니다.
               </span>
             )}
             {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete && (
               <span className="block mt-1 text-red-600">
-                ⚠️ GTC 조항에 {gtcCommentStatus.commentCount}개의 미해결 코멘트가 있어 서명할 수 없습니다.
+                ⚠️ {gtcCommentStatus.commentCount}개의 미해결 코멘트가 있어 서명할 수 없습니다.
               </span>
             )}
             {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && gtcCommentStatus.isComplete && (
               <span className="block mt-1 text-green-600">
-                ✅ GTC 조항 협의가 완료되어 서명 가능합니다.
+                ✅ 협의가 완료되어 서명 가능합니다.
               </span>
             )}
           </DialogDescription>
@@ -1603,12 +1594,28 @@ export function BasicContractSignViewer({
                 </div>
 
                 <div className={`absolute inset-0 p-3 ${activeTab === 'clauses' ? 'block' : 'hidden'}`}>
-                  {/* GTC 조항 컴포넌트 */}
-                  <GtcClausesComponent
-                    contractId={contractId}
-                    onCommentStatusChange={handleGtcCommentStatusChange}  // 메모이제이션된 콜백 사용
-                    t={t}
-                  />
+                  {/* 새로운 협의 코멘트 리스트 */}
+                  {contractId ? (
+                    <div className="h-full">
+                      <AgreementCommentList
+                        basicContractId={contractId}
+                        currentUserType={mode === 'vendor' ? 'Vendor' : 'SHI'}
+                        readOnly={false}
+                        onCommentCountChange={(count) => {
+                          handleGtcCommentStatusChange?.(
+                            count > 0,
+                            count,
+                            count > 0 ? 'negotiating' : 'draft',
+                            false
+                          );
+                        }}
+                      />
+                    </div>
+                  ) : (
+                    <div className="flex items-center justify-center h-full text-gray-500">
+                      <p>계약서 정보를 불러올 수 없습니다. (contractId: {String(contractId)})</p>
+                    </div>
+                  )}
                 </div>
 
                 <div className={`absolute inset-0 ${activeTab !== 'survey' && activeTab !== 'clauses' ? 'block' : 'hidden'}`}>
-- 
cgit v1.2.3

