From 02b1cf005cf3e1df64183d20ba42930eb2767a9f Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 21 Aug 2025 06:57:36 +0000
Subject: (대표님, 최겸) 설계메뉴추가, 작업사항 업데이트 설계메뉴 - 문서관리
 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보
 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정
 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정
 이메일 오류 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vendor-table/basic-contract-sign-dialog.tsx    | 313 +++++++++++++++------
 1 file changed, 229 insertions(+), 84 deletions(-)

(limited to 'lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx')

diff --git a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
index 7bffdac9..7d828a7e 100644
--- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
+++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
@@ -7,7 +7,6 @@ import { ScrollArea } from "@/components/ui/scroll-area";
 import { formatDate } from "@/lib/utils";
 import { toast } from "sonner";
 import { cn } from "@/lib/utils";
-import { BasicContractSignViewer } from "@/lib/basic-contract/viewer/basic-contract-sign-viewer";
 import type { WebViewerInstance } from "@pdftron/webviewer";
 import type { BasicContractView } from "@/db/schema";
 import {
@@ -19,45 +18,82 @@ import {
   FileText,
   User,
   AlertCircle,
-  Calendar
+  Calendar,
+  Loader2
 } from "lucide-react";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import { Input } from "@/components/ui/input";
 import { Badge } from "@/components/ui/badge";
 import { Separator } from "@/components/ui/separator";
 import { useRouter } from "next/navigation"
+import { BasicContractSignViewer } from "../viewer/basic-contract-sign-viewer";
+import { getVendorAttachments } from "../service";
 
-// 수정된 props 인터페이스
 interface BasicContractSignDialogProps {
   contracts: BasicContractView[];
   onSuccess?: () => void;
+  hasSelectedRows?: boolean;
+  t: (key: string) => string;
 }
 
-export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractSignDialogProps) {
+export function BasicContractSignDialog({ 
+  contracts, 
+  onSuccess, 
+  hasSelectedRows = false,
+  t 
+}: BasicContractSignDialogProps) {
   const [open, setOpen] = React.useState(false);
   const [selectedContract, setSelectedContract] = React.useState<BasicContractView | null>(null);
   const [instance, setInstance] = React.useState<null | WebViewerInstance>(null);
   const [searchTerm, setSearchTerm] = React.useState("");
   const [isSubmitting, setIsSubmitting] = React.useState(false);
+  
+  // 추가된 state들
+  const [additionalFiles, setAdditionalFiles] = React.useState<any[]>([]);
+  const [isLoadingAttachments, setIsLoadingAttachments] = React.useState(false);
+  
   const router = useRouter()
 
+  console.log(selectedContract,"selectedContract")
+  console.log(additionalFiles,"additionalFiles")
+  
+  // 버튼 비활성화 조건
+  const isButtonDisabled = !hasSelectedRows || contracts.length === 0;
+
+  // 비활성화 이유 텍스트
+  const getDisabledReason = () => {
+    if (!hasSelectedRows) {
+      return t("basicContracts.toolbar.selectRows");
+    }
+    if (contracts.length === 0) {
+      return t("basicContracts.toolbar.noPendingContracts");
+    }
+    return "";
+  };
+
   // 다이얼로그 열기/닫기 핸들러
   const handleOpenChange = (isOpen: boolean) => {
     setOpen(isOpen);
     
-    // 다이얼로그가 열릴 때 첫 번째 계약서 자동 선택
-    if (isOpen && contracts.length > 0 && !selectedContract) {
-      setSelectedContract(contracts[0]);
-    }
-    
     if (!isOpen) {
+      // 다이얼로그 닫을 때 상태 초기화
       setSelectedContract(null);
       setSearchTerm("");
+      setAdditionalFiles([]); // 추가 파일 상태 초기화
+      // WebViewer 인스턴스 정리
+      if (instance) {
+        try {
+          instance.UI.dispose();
+        } catch (error) {
+          console.log("WebViewer dispose error:", error);
+        }
+        setInstance(null);
+      }
     }
   };
 
   // 계약서 선택 핸들러
   const handleSelectContract = (contract: BasicContractView) => {
+    console.log("계약서 선택:", contract.id, contract.templateName);
     setSelectedContract(contract);
   };
 
@@ -79,6 +115,40 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
     }
   }, [open, contracts, selectedContract]);
 
+  // 추가 파일 가져오기 useEffect
+  React.useEffect(() => {
+    const fetchAdditionalFiles = async () => {
+      if (!selectedContract) {
+        setAdditionalFiles([]);
+        return;
+      }
+
+      // "비밀유지 계약서"인 경우에만 추가 파일 가져오기
+      if (selectedContract.templateName === "비밀유지 계약서") {
+        setIsLoadingAttachments(true);
+        try {
+          const result = await getVendorAttachments(selectedContract.vendorId);
+          if (result.success) {
+            setAdditionalFiles(result.data);
+            console.log("추가 파일 로드됨:", result.data);
+          } else {
+            console.error("Failed to fetch attachments:", result.error);
+            setAdditionalFiles([]);
+          }
+        } catch (error) {
+          console.error("Error fetching attachments:", error);
+          setAdditionalFiles([]);
+        } finally {
+          setIsLoadingAttachments(false);
+        }
+      } else {
+        setAdditionalFiles([]);
+      }
+    };
+
+    fetchAdditionalFiles();
+  }, [selectedContract]);
+
   // 서명 완료 핸들러
   const completeSign = async () => {
     if (!instance || !selectedContract) return;
@@ -89,29 +159,57 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
       const doc = documentViewer.getDocument();
       const xfdfString = await annotationManager.exportAnnotations();
 
+      // 폼 필드 데이터 수집
+      const fieldManager = annotationManager.getFieldManager();
+      const fields = fieldManager.getFields();
+      const formData: any = {};
+      fields.forEach((field: any) => {
+        formData[field.name] = field.value;
+      });
+
       const data = await doc.getFileData({
         xfdfString,
         downloadType: "pdf",
       });
       
       // FormData 생성 및 파일 추가
-      const formData = new FormData();
-      formData.append('file', new Blob([data], { type: 'application/pdf' }));
-      formData.append('tableRowId', selectedContract.id.toString());
-      formData.append('templateName', selectedContract.signedFileName || '');
+      const submitFormData = new FormData();
+      submitFormData.append('file', new Blob([data], { type: 'application/pdf' }));
+      submitFormData.append('tableRowId', selectedContract.id.toString());
+      submitFormData.append('templateName', selectedContract.signedFileName || '');
+      
+      // 폼 필드 데이터 추가
+      if (Object.keys(formData).length > 0) {
+        submitFormData.append('formData', JSON.stringify(formData));
+      }
+      
+      // 준법 템플릿인 경우 필수 필드 검증
+      if (selectedContract.templateName?.includes('준법')) {
+        const requiredFields = ['compliance_agreement', 'legal_review', 'risk_assessment'];
+        const missingFields = requiredFields.filter(field => !formData[field]);
+        
+        if (missingFields.length > 0) {
+          toast.error("필수 준법 항목이 누락되었습니다.", {
+            description: `다음 항목을 완료해주세요: ${missingFields.join(', ')}`,
+            icon: <AlertCircle className="h-5 w-5 text-red-500" />
+          });
+          setIsSubmitting(false);
+          return;
+        }
+      }
       
       // API 호출
       const response = await fetch('/api/upload/signed-contract', {
         method: 'POST',
-        body: formData,
+        body: submitFormData,
         next: { tags: ["basicContractView-vendor"] },
       });
       
       const result = await response.json();
 
       if (result.result) {
-        toast.success("서명이 성공적으로 완료되었습니다.", {
-          description: "문서가 성공적으로 처리되었습니다.",
+        toast.success(t("basicContracts.messages.signSuccess"), {
+          description: t("basicContracts.messages.documentProcessed"),
           icon: <CheckCircle2 className="h-5 w-5 text-green-500" />
         });
         router.refresh();
@@ -120,22 +218,19 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
           onSuccess();
         }
       } else {
-        toast.error("서명 처리 중 오류가 발생했습니다.", {
+        toast.error(t("basicContracts.messages.signError"), {
           description: result.error,
           icon: <AlertCircle className="h-5 w-5 text-red-500" />
         });
       }
     } catch (error) {
       console.error("서명 완료 중 오류:", error);
-      toast.error("서명 처리 중 오류가 발생했습니다.");
+      toast.error(t("basicContracts.messages.signError"));
     } finally {
       setIsSubmitting(false);
     }
   };
 
-  // 서명 대기중(PENDING) 계약서가 있는지 확인
-  const hasPendingContracts = contracts.length > 0;
-
   return (
     <>
       {/* 서명 버튼 */}
@@ -143,62 +238,67 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
         variant="outline"
         size="sm"
         onClick={() => setOpen(true)}
-        disabled={!hasPendingContracts}
-        className="gap-2 transition-all hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200"
+        disabled={isButtonDisabled}
+        className="gap-2 transition-all hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 disabled:opacity-50 disabled:cursor-not-allowed"
       >
-        <Upload className="size-4 text-blue-500" aria-hidden="true" />
+        <Upload 
+          className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-blue-500'}`} 
+          aria-hidden="true" 
+        />
         <span className="hidden sm:inline flex items-center">
-          서명하기
-          {contracts.length > 0 && (
+          {t("basicContracts.toolbar.sign")}
+          {contracts.length > 0 && !isButtonDisabled && (
             <Badge variant="secondary" className="ml-2 bg-blue-100 text-blue-700 hover:bg-blue-200">
               {contracts.length}
             </Badge>
           )}
+          {isButtonDisabled && (
+            <span className="ml-2 text-xs text-gray-400">
+              ({getDisabledReason()})
+            </span>
+          )}
         </span>
       </Button>
 
-      {/* 서명 다이얼로그 - 고정 높이 유지 */}
+      {/* 서명 다이얼로그 - 레이아웃 개선 */}
       <Dialog open={open} onOpenChange={handleOpenChange}>
-        <DialogContent className="max-w-5xl h-[650px] w-[90vw] p-0 overflow-hidden rounded-lg shadow-lg border border-gray-200">
-          <DialogHeader className="p-6 bg-gradient-to-r from-blue-50 to-purple-50 border-b">
+        <DialogContent className="max-w-7xl w-[95vw] h-[90vh] p-0 flex flex-col overflow-hidden" style={{width:'95vw', maxWidth:'95vw'}}>
+          {/* 고정 헤더 */}
+          <DialogHeader className="px-6 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b flex-shrink-0">
             <DialogTitle className="text-xl font-bold flex items-center text-gray-800">
               <FileSignature className="mr-2 h-5 w-5 text-blue-500" />
-              기본계약서 및 관련문서 서명
+              {t("basicContracts.dialog.title")}
+              {/* 추가 파일 로딩 표시 */}
+              {isLoadingAttachments && (
+                <Loader2 className="ml-2 h-4 w-4 animate-spin text-blue-500" />
+              )}
             </DialogTitle>
           </DialogHeader>
 
-          <div className="grid grid-cols-2 h-[calc(100%-4rem)] overflow-hidden">
-            {/* 왼쪽 영역 - 계약서 목록 */}
-            <div className="col-span-1 border-r border-gray-200 bg-gray-50">
-              <div className="p-4 border-b">
-                <div className="relative mb-10">
-                <div className="absolute inset-y-0 left-3.5 flex items-center pointer-events-none">
-                    <Search className="h-4 w-8 text-gray-400" />
+          {/* 메인 컨텐츠 영역 - Flexbox 사용 */}
+          <div className="flex flex-1 min-h-0 overflow-hidden">
+            {/* 왼쪽 영역 - 계약서 목록 (고정 너비) */}
+            <div className="w-80 border-r border-gray-200 bg-gray-50 flex flex-col flex-shrink-0">
+              <div className="p-3 border-b flex-shrink-0">
+                <div className="relative">
+                  <div className="absolute inset-y-0 left-2 flex items-center pointer-events-none">
+                    <Search className="h-4 w-4 text-gray-400" />
                   </div>
                   <Input
-                    placeholder="문서명 또는 요청자 검색"
-                    className="bg-white"
-                    style={{paddingLeft:25}}
+                    placeholder={t("basicContracts.dialog.searchPlaceholder")}
+                    className="bg-white pl-8 text-sm"
                     value={searchTerm}
                     onChange={(e) => setSearchTerm(e.target.value)}
                   />
                 </div>
-                <Tabs defaultValue="all" className="w-full">
-                  <TabsList className="w-full">
-                    <TabsTrigger value="all" className="flex-1">전체 ({contracts.length})</TabsTrigger>
-                    <TabsTrigger value="contracts" className="flex-1">계약서</TabsTrigger>
-                    <TabsTrigger value="docs" className="flex-1">관련문서</TabsTrigger>
-                  </TabsList>
-                </Tabs>
               </div>
 
-              <ScrollArea className="h-[calc(100%-6rem)]">
-                <div className="p-3">
+              <ScrollArea className="flex-1">
+                <div className="p-2">
                   {filteredContracts.length === 0 ? (
-                    <div className="flex flex-col items-center justify-center h-40 text-center">
-                      <FileText className="h-12 w-12 text-gray-300 mb-2" />
-                      <p className="text-gray-500 font-medium">서명 요청된 문서가 없습니다.</p>
-                      <p className="text-gray-400 text-sm mt-1">나중에 다시 확인해주세요.</p>
+                    <div className="flex flex-col items-center justify-center h-32 text-center">
+                      <FileText className="h-8 w-8 text-gray-300 mb-2" />
+                      <p className="text-gray-500 text-sm font-medium">{t("basicContracts.dialog.noDocuments")}</p>
                     </div>
                   ) : (
                     <div className="space-y-2">
@@ -207,30 +307,38 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
                           key={contract.id}
                           variant="outline"
                           className={cn(
-                            "w-full justify-start text-left h-auto p-3 bg-white hover:bg-blue-50 transition-colors",
+                            "w-full justify-start text-left h-auto p-2 bg-white hover:bg-blue-50 transition-colors",
                             "border border-gray-200 hover:border-blue-200 rounded-md",
                             selectedContract?.id === contract.id && "border-blue-500 bg-blue-50 shadow-sm"
                           )}
                           onClick={() => handleSelectContract(contract)}
                         >
-                          <div className="flex flex-col w-full">
+                          <div className="flex flex-col w-full space-y-1">
+                            {/* 첫 번째 줄: 제목 + 상태 */}
                             <div className="flex items-center justify-between w-full">
-                              <span className="font-semibold truncate text-gray-800 flex items-center">
-                                <FileText className="h-4 w-4 mr-2 text-blue-500" />
-                                {contract.templateName || '문서'}
+                              <span className="font-medium text-xs truncate text-gray-800 flex items-center min-w-0">
+                                <FileText className="h-3 w-3 mr-1 text-blue-500 flex-shrink-0" />
+                                <span className="truncate">{contract.templateName || t("basicContracts.dialog.document")}</span>
+                                {/* 비밀유지 계약서인 경우 표시 */}
+                                {contract.templateName === "비밀유지 계약서" && (
+                                  <Badge variant="outline" className="ml-1 bg-green-50 text-green-700 border-green-200 text-xs">
+                                    NDA
+                                  </Badge>
+                                )}
                               </span>
-                              <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200">
-                                대기중
+                              <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200 text-xs ml-2 flex-shrink-0">
+                                {t("basicContracts.statusValues.PENDING")}
                               </Badge>
                             </div>
-                            <Separator className="my-2 bg-gray-100" />
-                            <div className="grid grid-cols-2 gap-1 mt-1 text-xs text-gray-500">
-                              <div className="flex items-center">
-                                <User className="h-3 w-3 mr-1" />
-                                <span className="truncate">{contract.requestedByName || '알 수 없음'}</span>
+                            
+                            {/* 두 번째 줄: 사용자 + 날짜 */}
+                            <div className="flex items-center justify-between text-xs text-gray-500">
+                              <div className="flex items-center min-w-0">
+                                <User className="h-3 w-3 mr-1 flex-shrink-0" />
+                                <span className="truncate">{contract.requestedByName || t("basicContracts.dialog.unknown")}</span>
                               </div>
-                              <div className="flex items-center justify-end">
-                                <Calendar className="h-3 w-3 mr-1" />
+                              <div className="flex items-center ml-2 flex-shrink-0">
+                                <Calendar className="h-3 w-3 mr-1 flex-shrink-0" />
                                 <span>{formatDate(contract.createdAt)}</span>
                               </div>
                             </div>
@@ -243,19 +351,32 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
               </ScrollArea>
             </div>
 
-            {/* 오른쪽 영역 - 문서 뷰어 */}
-            <div className="col-span-1 bg-white flex flex-col h-full">
+            {/* 오른쪽 영역 - 문서 뷰어 (확장 가능) */}
+            <div className="flex-1 bg-white flex flex-col min-w-0">
               {selectedContract ? (
                 <>
-                  <div className="p-3 border-b bg-gray-50">
+                  {/* 뷰어 헤더 */}
+                  <div className="p-4 border-b bg-gray-50 flex-shrink-0">
                     <h3 className="font-semibold text-gray-800 flex items-center">
                       <FileText className="h-4 w-4 mr-2 text-blue-500" />
-                      {selectedContract.templateName || '문서'}
+                      {selectedContract.templateName || t("basicContracts.dialog.document")}
+                      {/* 준법 템플릿 표시 */}
+                      {selectedContract.templateName?.includes('준법') && (
+                        <Badge variant="outline" className="ml-2 bg-amber-50 text-amber-700 border-amber-200">
+                          준법 서류
+                        </Badge>
+                      )}
+                      {/* 비밀유지 계약서인 경우 추가 파일 수 표시 */}
+                      {selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && (
+                        <Badge variant="outline" className="ml-2 bg-blue-50 text-blue-700 border-blue-200">
+                          첨부파일 {additionalFiles.length}개
+                        </Badge>
+                      )}
                     </h3>
-                    <div className="flex justify-between items-center mt-1 text-xs text-gray-500">
+                    <div className="flex justify-between items-center mt-2 text-sm text-gray-500">
                       <span className="flex items-center">
                         <User className="h-3 w-3 mr-1" />
-                        요청자: {selectedContract.requestedByName || '알 수 없음'}
+                        {t("basicContracts.dialog.requester")}: {selectedContract.requestedByName || t("basicContracts.dialog.unknown")}
                       </span>
                       <span className="flex items-center">
                         <Clock className="h-3 w-3 mr-1" />
@@ -263,19 +384,43 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
                       </span>
                     </div>
                   </div>
-                  <div className="flex-grow overflow-hidden border-b">
+                  
+                  {/* 뷰어 영역 - 남은 공간 모두 사용 */}
+                  <div className="flex-1 min-h-0 overflow-hidden">
                     <BasicContractSignViewer
+                      key={selectedContract.id} // key 추가로 컴포넌트 재생성 강제
                       contractId={selectedContract.id}
                       filePath={selectedContract.signedFilePath || undefined}
+                      templateName={selectedContract.templateName || ""}
+                      additionalFiles={additionalFiles} // 추가 파일 전달
                       instance={instance}
                       setInstance={setInstance}
+                      t={t}
                     />
                   </div>
-                  <div className="p-3 flex justify-between items-center bg-gray-50">
-                    <p className="text-sm text-gray-600">
-                      <AlertCircle className="h-4 w-4 text-yellow-500 inline mr-1" />
-                      서명 후에는 변경할 수 없습니다.
-                    </p>
+
+                  {/* 고정 푸터 */}
+                  <div className="p-4 flex justify-between items-center bg-gray-50 border-t flex-shrink-0">
+                    <div className="flex items-center space-x-4">
+                      <p className="text-sm text-gray-600 flex items-center">
+                        <AlertCircle className="h-4 w-4 text-yellow-500 mr-1" />
+                        {t("basicContracts.dialog.signWarning")}
+                      </p>
+                      {/* 준법 템플릿인 경우 추가 안내 */}
+                      {selectedContract.templateName?.includes('준법') && (
+                        <p className="text-xs text-amber-600 flex items-center">
+                          <AlertCircle className="h-3 w-3 text-amber-500 mr-1" />
+                          모든 준법 항목을 체크해주세요
+                        </p>
+                      )}
+                      {/* 비밀유지 계약서인 경우 추가 안내 */}
+                      {selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && (
+                        <p className="text-xs text-blue-600 flex items-center">
+                          <FileText className="h-3 w-3 text-blue-500 mr-1" />
+                          첨부 서류도 확인해주세요
+                        </p>
+                      )}
+                    </div>
                     <Button
                       className="gap-2 bg-blue-600 hover:bg-blue-700 transition-colors"
                       onClick={completeSign}
@@ -287,12 +432,12 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
                             <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                             <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                           </svg>
-                          처리 중...
+                          {t("basicContracts.dialog.processing")}
                         </>
                       ) : (
                         <>
                           <FileSignature className="h-4 w-4" />
-                          서명 완료
+                          {t("basicContracts.dialog.completeSign")}
                         </>
                       )}
                     </Button>
@@ -303,9 +448,9 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS
                   <div className="bg-blue-50 p-6 rounded-full mb-4">
                     <FileSignature className="h-12 w-12 text-blue-500" />
                   </div>
-                  <h3 className="text-xl font-medium text-gray-800 mb-2">문서를 선택해주세요</h3>
+                  <h3 className="text-xl font-medium text-gray-800 mb-2">{t("basicContracts.dialog.selectDocument")}</h3>
                   <p className="text-gray-500 max-w-md">
-                    왼쪽 목록에서 서명할 문서를 선택하면 여기에 문서 내용이 표시됩니다.
+                    {t("basicContracts.dialog.selectDocumentDescription")}
                   </p>
                 </div>
               )}
-- 
cgit v1.2.3

