From 77cbcaf27c9de8b361a6c5a13f0eefb37fd0d0e5 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 20 Nov 2025 02:52:16 +0000
Subject: (임수민) 기본계약서 서명 위치, 설문조사 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../viewer/basic-contract-sign-viewer.tsx          | 164 ++++++++++++++++-----
 1 file changed, 129 insertions(+), 35 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 7f5fa027..adc735e9 100644
--- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
+++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
@@ -697,6 +697,9 @@ export function BasicContractSignViewer({
 }: BasicContractSignViewerProps) {
   const { toast } = useToast();
 
+  // 🔽 추가
+  const signatureFieldsRef = useRef<string[]>([]);
+
   const [fileLoading, setFileLoading] = useState<boolean>(true);
   const [activeTab, setActiveTab] = useState<string>("main");
   const [surveyData, setSurveyData] = useState<any>({});
@@ -822,6 +825,11 @@ export function BasicContractSignViewer({
     setTimeout(() => cleanupHtmlStyle(), 100);
   };
 
+  // 🔽 최신 서명 필드 이름들을 ref에 동기화
+  useEffect(() => {
+    signatureFieldsRef.current = signatureFields;
+  }, [signatureFields]);
+
   useEffect(() => {
     setShowDialog(isOpen);
 
@@ -960,33 +968,57 @@ export function BasicContractSignViewer({
 
             documentViewer.addEventListener('documentLoaded', handleDocumentLoaded);
 
-            // 구매자 모드가 아닐 때만 자동 서명 적용
-            if (mode !== 'buyer') {
-              annotationManager.addEventListener('annotationChanged', async (annotList, type) => {
-                for (const annot of annotList) {
-                  const { fieldName, X, Y, Width, Height, PageNumber } = annot;
-
-                  if (type === "add" && annot.Subject === "Widget") {
-                    const signatureImage = await getVendorSignatureFile()
-
-                    const stamp = new Annotations.StampAnnotation();
-                    stamp.PageNumber = PageNumber;
-                    stamp.X = X;
-                    stamp.Y = Y;
-                    stamp.Width = Width;
-                    stamp.Height = Height;
-
-                    const dataUrl = signatureImage?.data?.dataUrl;
-                    if (dataUrl) {
-                      await stamp.setImageData(dataUrl);
-                      annot.sign(stamp);
-                      annot.setFieldFlag(WidgetFlags.READ_ONLY, true);
-                    }
-
+            // 🔁 서명 관련 annotation 제어
+            annotationManager.addEventListener('annotationChanged', async (annotList, type) => {
+              if (type !== 'add') return;
+
+              for (const annot of annotList) {
+                const isWidgetSignature =
+                  annot instanceof Annotations.SignatureWidgetAnnotation;
+
+                // 1) 필드 없는(Signature) 서명은 전부 막기
+                //    - WebViewer 기본 "어디나 한 번 찍는" 서명은 보통 Subject 가 "Signature" 로 들어옵니다.
+                //    - 혹시 다르면 console.log(annot) 찍어서 Subject 를 맞춰주면 됩니다.
+                if (!isWidgetSignature) {
+                  if (annot.Subject === 'Signature') {
+                    // 지정된 서명란 외 서명 → 즉시 삭제
+                    annotationManager.deleteAnnotation(annot, false);
                   }
+                  continue;
                 }
-              });
-            }
+
+                // 2) 서명 위젯(필드)은 "우리가 생성한 필드"만 허용
+                const field = annot.getField && annot.getField();
+                const name = field?.name as string | undefined;
+                const allowed = signatureFieldsRef.current;
+
+                if (name && allowed.length > 0 && !allowed.includes(name)) {
+                  // 우리가 만든 서명 필드가 아니면 막기
+                  annotationManager.deleteAnnotation(annot, false);       
+                  continue;
+                }
+
+                // 3) 여기까지 통과한 경우 = 우리가 만든 서명 필드에 대한 서명
+                //    → 기존 자동 서명(협력사 서명) 로직 유지
+                if (mode !== 'buyer') {
+                  const signatureImage = await getVendorSignatureFile();
+                  if (!signatureImage?.data?.dataUrl) continue;
+
+                  const stamp = new Annotations.StampAnnotation();
+                  stamp.PageNumber = annot.PageNumber;
+                  stamp.X = annot.X;
+                  stamp.Y = annot.Y;
+                  stamp.Width = annot.Width;
+                  stamp.Height = annot.Height;
+
+                  await stamp.setImageData(signatureImage.data.dataUrl);
+
+                  const { WidgetFlags } = Annotations;
+                  annot.sign(stamp);
+                  annot.setFieldFlag(WidgetFlags.READ_ONLY, true);
+                }
+              }
+            });
 
             newInstance.UI.setMinZoomLevel('25%');
             newInstance.UI.setMaxZoomLevel('400%');
@@ -1329,6 +1361,15 @@ export function BasicContractSignViewer({
         {/* 구매자 모드에서는 탭 없이 단일 뷰어만 표시 */}
         {allFiles.length > 1 && mode !== 'buyer' ? (
           <Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
+            {/* 준법설문 미완료 알림 배너 */}
+            {isComplianceTemplate && !surveyData.completed && (
+              <div className="bg-amber-50 border-b-2 border-amber-400 px-4 py-2 flex items-center justify-center space-x-2">
+                <AlertTriangle className="h-4 w-4 text-amber-600" />
+                <span className="text-sm font-semibold text-amber-700">
+                  ⚠️ 준법 설문조사를 먼저 완료해주세요. 서명을 진행하려면 "준법 설문조사" 탭을 클릭하세요.
+                </span>
+              </div>
+            )}
             <div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0">
               <TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}>
                 {allFiles.map((file, index) => {
@@ -1344,19 +1385,41 @@ export function BasicContractSignViewer({
                     tabId = `file-${fileOnlyIndex}`;
                   }
 
+                  const isSurveyTab = file.type === 'survey';
+                  const isSurveyIncomplete = isSurveyTab && !surveyData.completed;
+                  
                   return (
-                    <TabsTrigger key={tabId} value={tabId} className="text-xs">
+                    <TabsTrigger 
+                      key={tabId} 
+                      value={tabId} 
+                      className={`text-xs relative ${
+                        isSurveyIncomplete 
+                          ? 'bg-amber-50 border-2 border-amber-400 shadow-md' 
+                          : isSurveyTab 
+                          ? 'bg-blue-50 border border-blue-300' 
+                          : ''
+                      }`}
+                    >
                       <div className="flex items-center space-x-1">
                         {file.type === 'survey' ? (
-                          <ClipboardList className="h-3 w-3" />
+                          <ClipboardList className={`h-3 w-3 ${isSurveyIncomplete ? 'text-amber-600' : 'text-blue-600'}`} />
                         ) : file.type === 'clauses' ? (
                           <BookOpen className="h-3 w-3" />
                         ) : (
                           <FileText className="h-3 w-3" />
                         )}
-                        <span className="truncate">{file.name}</span>
-                        {file.type === 'survey' && surveyData.completed && (
-                          <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">완료</Badge>
+                        <span className={`truncate font-medium ${isSurveyIncomplete ? 'text-amber-700' : ''}`}>
+                          {file.name}
+                        </span>
+                        {isSurveyTab && !surveyData.completed && (
+                          <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-amber-500 text-white animate-bounce">
+                            필수
+                          </Badge>
+                        )}
+                        {isSurveyTab && surveyData.completed && (
+                          <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs bg-green-500 text-white">
+                            완료
+                          </Badge>
                         )}
                         {file.type === 'clauses' && gtcCommentStatus.hasComments && (
                           <Badge variant="destructive" className="ml-1 h-4 px-1 text-xs">
@@ -1529,6 +1592,15 @@ export function BasicContractSignViewer({
           {/* 구매자 모드에서는 탭 없이 단일 뷰어만 표시 */}
           {allFiles.length > 1 && mode !== 'buyer' ? (
             <Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
+              {/* 준법설문 미완료 알림 배너 */}
+              {isComplianceTemplate && !surveyData.completed && (
+                <div className="bg-amber-50 border-b-2 border-amber-400 px-4 py-2 flex items-center justify-center space-x-2">
+                  <AlertTriangle className="h-4 w-4 text-amber-600" />
+                  <span className="text-sm font-semibold text-amber-700">
+                    ⚠️ 준법 설문조사를 먼저 완료해주세요. 서명을 진행하려면 "준법 설문조사" 탭을 클릭하세요.
+                  </span>
+                </div>
+              )}
               <div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0">
                 <TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}>
                   {allFiles.map((file, index) => {
@@ -1544,19 +1616,41 @@ export function BasicContractSignViewer({
                       tabId = `file-${fileOnlyIndex}`;
                     }
 
+                    const isSurveyTab = file.type === 'survey';
+                    const isSurveyIncomplete = isSurveyTab && !surveyData.completed;
+                    
                     return (
-                      <TabsTrigger key={tabId} value={tabId} className="text-xs">
+                      <TabsTrigger 
+                        key={tabId} 
+                        value={tabId} 
+                        className={`text-xs relative ${
+                          isSurveyIncomplete 
+                            ? 'bg-amber-50 border-2 border-amber-400 shadow-md' 
+                            : isSurveyTab 
+                            ? 'bg-blue-50 border border-blue-300' 
+                            : ''
+                        }`}
+                      >
                         <div className="flex items-center space-x-1">
                           {file.type === 'survey' ? (
-                            <ClipboardList className="h-3 w-3" />
+                            <ClipboardList className={`h-3 w-3 ${isSurveyIncomplete ? 'text-amber-600' : 'text-blue-600'}`} />
                           ) : file.type === 'clauses' ? (
                             <BookOpen className="h-3 w-3" />
                           ) : (
                             <FileText className="h-3 w-3" />
                           )}
-                          <span className="truncate">{file.name}</span>
-                          {file.type === 'survey' && surveyData.completed && (
-                            <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">완료</Badge>
+                          <span className={`truncate font-medium ${isSurveyIncomplete ? 'text-amber-700' : ''}`}>
+                            {file.name}
+                          </span>
+                          {isSurveyTab && !surveyData.completed && (
+                            <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-amber-500 text-white animate-bounce">
+                              필수
+                            </Badge>
+                          )}
+                          {isSurveyTab && surveyData.completed && (
+                            <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs bg-green-500 text-white">
+                              완료
+                            </Badge>
                           )}
                           {file.type === 'clauses' && gtcCommentStatus.hasComments && (
                             <Badge variant="destructive" className="ml-1 h-4 px-1 text-xs">
-- 
cgit v1.2.3

