From ea8aed1e1d62fb9fa6716347de73e4ef13040929 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Tue, 9 Dec 2025 05:35:23 +0000
Subject: (임수민) 공동인증서 개발
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vendor-table/basic-contract-sign-dialog.tsx    | 138 ++++++++++++++++++++-
 1 file changed, 136 insertions(+), 2 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 407a3c4d..3a6dcf9b 100644
--- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
+++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
@@ -31,6 +31,7 @@ import { Separator } from "@/components/ui/separator";
 import { useRouter } from "next/navigation"
 import { BasicContractSignViewer } from "../viewer/basic-contract-sign-viewer";
 import { getVendorAttachments, processBuyerSignatureAction } from "../service";
+import { getVendorInfo } from "../service-vendor-info";
 
 // 계약서 상태 타입 정의
 interface ContractStatus {
@@ -77,6 +78,7 @@ export function BasicContractSignDialog({
 
   // 계약서 상태 관리
   const [contractStatuses, setContractStatuses] = React.useState<ContractStatus[]>([]);
+  const [vendorInfo, setVendorInfo] = React.useState<{taxId?: string | null, country?: string | null} | null>(null);
 
   // 서명/설문/GTC 코멘트 완료 상태 관리
   const [surveyCompletionStatus, setSurveyCompletionStatus] = React.useState<Record<number, boolean>>({});
@@ -97,7 +99,13 @@ export function BasicContractSignDialog({
   // 모드에 따른 텍스트
   const isBuyerMode = mode === 'buyer';
   const dialogTitle = isBuyerMode ? "구매자 최종승인 서명" : t("basicContracts.dialog.title");
-  const signButtonText = isBuyerMode ? "최종승인 완료" : "서명 완료 및 저장";
+  
+  // 서명 버튼 텍스트 (내자/외자 구분)
+  const signButtonText = React.useMemo(() => {
+    if (isBuyerMode) return "최종승인 완료";
+    if (vendorInfo?.country === 'KR') return "공동인증 서명";
+    return "서명 완료 및 저장";
+  }, [isBuyerMode, vendorInfo]);
 
   // 버튼 비활성화 조건
   const isButtonDisabled = !hasSelectedRows || contracts.length === 0;
@@ -257,15 +265,26 @@ const canCompleteCurrentContract = React.useMemo(() => {
   React.useEffect(() => {
     if (isBuyerMode) {
       setAdditionalFiles([]);
+      setVendorInfo(null);
       return;
     }
 
     const fetchAdditionalFiles = async () => {
       if (!selectedContract) {
         setAdditionalFiles([]);
+        setVendorInfo(null);
         return;
       }
 
+      // 벤더 정보 가져오기 (사업자번호 등)
+      if (selectedContract.vendorId) {
+        getVendorInfo(selectedContract.vendorId).then(res => {
+          if (res.success && res.data) {
+            setVendorInfo(res.data);
+          }
+        });
+      }
+
       // "비밀유지 계약서"인 경우에만 추가 파일 가져오기
       if (selectedContract.templateName === "비밀유지 계약서" && selectedContract.vendorId) {
         setIsLoadingAttachments(true);
@@ -329,6 +348,104 @@ const canCompleteCurrentContract = React.useMemo(() => {
   const completeSign = async () => {
     if (!instance || !selectedContract) return;
 
+    // 🔹 내자인 경우 공동인증 팝업 열기
+    if (!isBuyerMode && vendorInfo?.country === 'KR') {
+      console.log("🔐 내자 협력업체 - 공동인증 서명 시작");
+      
+      // 검증 로직은 동일하게 수행
+      const contractId = selectedContract.id;
+      const isComplianceTemplate = selectedContract.templateName?.includes('준법');
+      const isGTCTemplate = selectedContract.templateName?.includes('GTC');
+      const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true;
+      const requiresNegotiationComplete = isComplianceTemplate || isGTCTemplate;
+      const negotiationStatus = gtcCommentStatus[contractId];
+      const negotiationCleared = requiresNegotiationComplete
+        ? (!negotiationStatus?.hasComments || negotiationStatus?.isComplete === true)
+        : true;
+
+      if (!surveyCompleted) {
+        toast({
+          title: "준법 설문조사를 먼저 완료해주세요.",
+          description: "설문조사 탭에서 모든 필수 항목을 완료해주세요.",
+          variant: "destructive"
+        });
+        return;
+      }
+
+      if (!negotiationCleared) {
+        toast({
+          title: "코멘트가 있어 서명할 수 없습니다.",
+          description: "협의 코멘트 탭에서 모든 코멘트를 삭제하거나 협의를 완료해주세요.",
+          variant: "destructive"
+        });
+        return;
+      }
+
+      // 공동인증 팝업 열기
+      // trustnet은 포트 없이 접근 (<ip>:<port> → <ip>/trustnet)
+      // 단, API 호출을 위해 부모 창의 origin (포트 포함)을 전달
+      const ssn = vendorInfo.taxId || '';
+      const ssnParam = ssn ? `&ssn=${encodeURIComponent(ssn)}` : '';
+      const baseUrl = `${window.location.protocol}//${window.location.hostname}`;
+      const apiOrigin = window.location.origin; // 포트 포함 (예: http://60.101.108.100:3001)
+      const popupUrl = `${baseUrl}/trustnet?contractId=${selectedContract.id}${ssnParam}&autoStart=true&apiOrigin=${encodeURIComponent(apiOrigin)}`;
+      
+      console.log('🔐 공동인증 팝업 열기:', { baseUrl, apiOrigin, popupUrl, ssn });
+      const popupWidth = 600;
+      const popupHeight = 700;
+      const left = (window.screen.width - popupWidth) / 2;
+      const top = (window.screen.height - popupHeight) / 2;
+      
+      const popup = window.open(
+        popupUrl,
+        'ContractSign',
+        `width=${popupWidth},height=${popupHeight},left=${left},top=${top},resizable=yes,scrollbars=yes`
+      );
+
+      if (!popup) {
+        toast({
+          title: "팝업이 차단되었습니다",
+          description: "브라우저 팝업 차단을 해제해주세요.",
+          variant: "destructive"
+        });
+        return;
+      }
+
+      toast({
+        title: "공동인증 서명 창이 열렸습니다",
+        description: "공동인증서로 서명을 진행해주세요.",
+        duration: 3000 // 3초 후 자동으로 사라짐
+      });
+
+      // 팝업에서 서명 완료 메시지를 받으면 새로고침
+      const handleMessage = (event: MessageEvent) => {
+        console.log('📨 메시지 수신:', event.data);
+        
+        if (event.data.type === 'INTERNAL_SIGN_COMPLETE') {
+          console.log('✅ 공동인증 서명 완료:', event.data.contractId);
+          
+          toast({
+            title: "서명이 완료되었습니다",
+            description: "계약서 상태가 업데이트됩니다.",
+            duration: 3000 // 3초 후 자동으로 사라짐
+          });
+
+          // 팝업 닫힌 후 테이블 데이터만 재조회 (페이지 전체 리로드 대신)
+          if (onSuccess) {
+            onSuccess();
+          }
+          router.refresh();
+
+          // 리스너 제거
+          window.removeEventListener('message', handleMessage);
+        }
+      };
+
+      window.addEventListener('message', handleMessage);
+
+      return;
+    }
+
     // 서명 완료 가능 여부 재확인
     if (!canCompleteCurrentContract) {
       const contractId = selectedContract.id;
@@ -345,7 +462,7 @@ const canCompleteCurrentContract = React.useMemo(() => {
           return;
         }
       } else {
-        // 협력업체 모드의 기존 검증 로직
+        // 협력업체 모드의 기존 검증 로직 (외자)
         const isComplianceTemplate = selectedContract.templateName?.includes('준법');
         const isGTCTemplate = selectedContract.templateName?.includes('GTC');
         const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true;
@@ -882,6 +999,21 @@ const canCompleteCurrentContract = React.useMemo(() => {
                         </Badge>
                       )}
 
+                      {/* 내자/외자 표시 (협력업체 모드일 때만) */}
+                      {!isBuyerMode && vendorInfo && (
+                        <Badge 
+                          variant="outline" 
+                          className={cn(
+                            "ml-2",
+                            vendorInfo.country === 'KR' 
+                              ? "bg-blue-50 text-blue-700 border-blue-200" 
+                              : "bg-gray-50 text-gray-700 border-gray-200"
+                          )}
+                        >
+                          {vendorInfo.country === 'KR' ? '🔐 내자 (공동인증)' : '🌍 외자'}
+                        </Badge>
+                      )}
+
                       {/* 준법/GTC 템플릿 표시 (구매자 모드가 아닐 때만) */}
                       {!isBuyerMode && selectedContract.templateName?.includes('준법') && (
                         <Badge variant="outline" className="ml-2 bg-amber-50 text-amber-700 border-amber-200">
@@ -932,6 +1064,8 @@ const canCompleteCurrentContract = React.useMemo(() => {
                       mode={mode}
                       t={t}
                       negotiationCompletedAt={(selectedContract as any).negotiationCompletedAt || null}
+                      vendorTaxId={vendorInfo?.taxId || undefined}
+                      vendorCountry={vendorInfo?.country || undefined}
                     />
                   </div>
 
-- 
cgit v1.2.3

