From 4eb7532f822c821fb6b69bf103bd075fefba769b Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Tue, 15 Jul 2025 10:07:09 +0000
Subject: (대표님) 20250715 협력사 정기평가, spreadJS, roles 서비스에 함수 추가
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../ship/send-to-shi-button.tsx                    | 336 +++++++++++++++------
 1 file changed, 240 insertions(+), 96 deletions(-)

(limited to 'lib/vendor-document-list/ship/send-to-shi-button.tsx')

diff --git a/lib/vendor-document-list/ship/send-to-shi-button.tsx b/lib/vendor-document-list/ship/send-to-shi-button.tsx
index 1a27a794..61893da5 100644
--- a/lib/vendor-document-list/ship/send-to-shi-button.tsx
+++ b/lib/vendor-document-list/ship/send-to-shi-button.tsx
@@ -1,4 +1,4 @@
-// components/sync/send-to-shi-button.tsx (최종 버전)
+// components/sync/send-to-shi-button.tsx (다중 계약 버전)
 "use client"
 
 import * as React from "react"
@@ -21,33 +21,59 @@ import {
 import { Badge } from "@/components/ui/badge"
 import { Progress } from "@/components/ui/progress"
 import { Separator } from "@/components/ui/separator"
+import { ScrollArea } from "@/components/ui/scroll-area"
 import { useSyncStatus, useTriggerSync } from "@/hooks/use-sync-status"
 import type { EnhancedDocument } from "@/types/enhanced-documents"
 
 interface SendToSHIButtonProps {
-  contractId: number
   documents?: EnhancedDocument[]
   onSyncComplete?: () => void
   projectType: "ship" | "plant"
 }
 
+interface ContractSyncStatus {
+  contractId: number
+  syncStatus: any
+  isLoading: boolean
+  error: any
+}
+
 export function SendToSHIButton({ 
-  contractId, 
   documents = [], 
   onSyncComplete,
   projectType
 }: SendToSHIButtonProps) {
   const [isDialogOpen, setIsDialogOpen] = React.useState(false)
   const [syncProgress, setSyncProgress] = React.useState(0)
+  const [currentSyncingContract, setCurrentSyncingContract] = React.useState<number | null>(null)
 
-  const targetSystem = projectType === 'ship'?"DOLCE":"SWP"
+  const targetSystem = projectType === 'ship' ? "DOLCE" : "SWP"
   
-  const { 
-    syncStatus, 
-    isLoading: statusLoading, 
-    error: statusError,
-    refetch: refetchStatus 
-  } = useSyncStatus(contractId, targetSystem)
+  // documents에서 contractId 목록 추출
+  const documentsContractIds = React.useMemo(() => {
+    const uniqueIds = [...new Set(documents.map(doc => doc.contractId).filter(Boolean))]
+    return uniqueIds.sort()
+  }, [documents])
+
+  // 각 contract별 동기화 상태 조회
+  const contractStatuses = React.useMemo(() => {
+    return documentsContractIds.map(contractId => {
+      const { 
+        syncStatus, 
+        isLoading, 
+        error,
+        refetch 
+      } = useSyncStatus(contractId, targetSystem)
+      
+      return {
+        contractId,
+        syncStatus,
+        isLoading,
+        error,
+        refetch
+      }
+    })
+  }, [documentsContractIds, targetSystem])
 
   const { 
     triggerSync, 
@@ -55,60 +81,130 @@ export function SendToSHIButton({
     error: syncError 
   } = useTriggerSync()
 
+  // 전체 통계 계산
+  const totalStats = React.useMemo(() => {
+    let totalPending = 0
+    let totalSynced = 0
+    let totalFailed = 0
+    let hasError = false
+    let isLoading = false
+
+    contractStatuses.forEach(({ syncStatus, error, isLoading: loading }) => {
+      if (error) hasError = true
+      if (loading) isLoading = true
+      if (syncStatus) {
+        totalPending += syncStatus.pendingChanges || 0
+        totalSynced += syncStatus.syncedChanges || 0
+        totalFailed += syncStatus.failedChanges || 0
+      }
+    })
+
+    return {
+      totalPending,
+      totalSynced,
+      totalFailed,
+      hasError,
+      isLoading,
+      canSync: totalPending > 0 && !hasError
+    }
+  }, [contractStatuses])
+
   // 에러 상태 표시
   React.useEffect(() => {
-    if (statusError) {
-      console.warn('Failed to load sync status:', statusError)
+    if (totalStats.hasError) {
+      console.warn('Failed to load sync status for some contracts')
     }
-  }, [statusError])
+  }, [totalStats.hasError])
 
   const handleSync = async () => {
-    if (!contractId) return
+    if (documentsContractIds.length === 0) return
     
     setSyncProgress(0)
+    let successfulSyncs = 0
+    let failedSyncs = 0
+    let totalSuccessCount = 0
+    let totalFailureCount = 0
+    const errors: string[] = []
     
     try {
-      // 진행률 시뮬레이션
-      const progressInterval = setInterval(() => {
-        setSyncProgress(prev => Math.min(prev + 10, 90))
-      }, 200)
-
-      const result = await triggerSync({ 
-        contractId, 
-        targetSystem 
-      })
-      
-      clearInterval(progressInterval)
-      setSyncProgress(100)
+      const contractsToSync = contractStatuses.filter(
+        ({ syncStatus, error }) => !error && syncStatus?.syncEnabled && syncStatus?.pendingChanges > 0
+      )
+
+      if (contractsToSync.length === 0) {
+        toast.info('동기화할 변경사항이 없습니다.')
+        setIsDialogOpen(false)
+        return
+      }
+
+      // 각 contract별로 순차 동기화
+      for (let i = 0; i < contractsToSync.length; i++) {
+        const { contractId } = contractsToSync[i]
+        setCurrentSyncingContract(contractId)
+        
+        try {
+          const result = await triggerSync({ 
+            contractId, 
+            targetSystem 
+          })
+          
+          if (result?.success) {
+            successfulSyncs++
+            totalSuccessCount += result.successCount || 0
+          } else {
+            failedSyncs++
+            totalFailureCount += result?.failureCount || 0
+            if (result?.errors?.[0]) {
+              errors.push(`Contract ${contractId}: ${result.errors[0]}`)
+            }
+          }
+        } catch (error) {
+          failedSyncs++
+          const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류'
+          errors.push(`Contract ${contractId}: ${errorMessage}`)
+        }
+
+        // 진행률 업데이트
+        setSyncProgress(((i + 1) / contractsToSync.length) * 100)
+      }
+
+      setCurrentSyncingContract(null)
       
       setTimeout(() => {
         setSyncProgress(0)
         setIsDialogOpen(false)
         
-        if (result?.success) {
+        if (failedSyncs === 0) {
           toast.success(
-            `동기화 완료: ${result.successCount || 0}건 성공`,
+            `모든 계약 동기화 완료: ${totalSuccessCount}건 성공`,
+            {
+              description: `${successfulSyncs}개 계약에서 ${totalSuccessCount}개 항목이 SHI 시스템으로 전송되었습니다.`
+            }
+          )
+        } else if (successfulSyncs > 0) {
+          toast.warning(
+            `부분 동기화 완료: ${successfulSyncs}개 성공, ${failedSyncs}개 실패`,
             {
-              description: result.successCount > 0 
-                ? `${result.successCount}개 항목이 SHI 시스템으로 전송되었습니다.`
-                : '전송할 새로운 변경사항이 없습니다.'
+              description: errors[0] || '일부 계약 동기화에 실패했습니다.'
             }
           )
         } else {
           toast.error(
-            `동기화 부분 실패: ${result?.successCount || 0}건 성공, ${result?.failureCount || 0}건 실패`,
+            `동기화 실패: ${failedSyncs}개 계약 모두 실패`,
             {
-              description: result?.errors?.[0] || '일부 항목 전송에 실패했습니다.'
+              description: errors[0] || '모든 계약 동기화에 실패했습니다.'
             }
           )
         }
         
-        refetchStatus() // SWR 캐시 갱신
+        // 모든 contract 상태 갱신
+        contractStatuses.forEach(({ refetch }) => refetch?.())
         onSyncComplete?.()
       }, 500)
       
     } catch (error) {
       setSyncProgress(0)
+      setCurrentSyncingContract(null)
       
       toast.error('동기화 실패', {
         description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.'
@@ -117,28 +213,28 @@ export function SendToSHIButton({
   }
 
   const getSyncStatusBadge = () => {
-    if (statusLoading) {
+    if (totalStats.isLoading) {
       return <Badge variant="secondary">확인 중...</Badge>
     }
 
-    if (statusError) {
+    if (totalStats.hasError) {
       return <Badge variant="destructive">오류</Badge>
     }
 
-    if (!syncStatus) {
-      return <Badge variant="secondary">데이터 없음</Badge>
+    if (documentsContractIds.length === 0) {
+      return <Badge variant="secondary">계약 없음</Badge>
     }
 
-    if (syncStatus.pendingChanges > 0) {
+    if (totalStats.totalPending > 0) {
       return (
         <Badge variant="destructive" className="gap-1">
           <AlertTriangle className="w-3 h-3" />
-          {syncStatus.pendingChanges}건 대기
+          {totalStats.totalPending}건 대기
         </Badge>
       )
     }
 
-    if (syncStatus.syncedChanges > 0) {
+    if (totalStats.totalSynced > 0) {
       return (
         <Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600">
           <CheckCircle className="w-3 h-3" />
@@ -150,86 +246,116 @@ export function SendToSHIButton({
     return <Badge variant="secondary">변경사항 없음</Badge>
   }
 
-  const canSync = !statusError && syncStatus?.syncEnabled && syncStatus?.pendingChanges > 0
+  const refreshAllStatuses = () => {
+    contractStatuses.forEach(({ refetch }) => refetch?.())
+  }
 
   return (
     <>
       <Popover>
         <PopoverTrigger asChild>
-        <div className="flex items-center gap-3">
-          <Button
-            variant="default"
-            size="sm"
-            className="flex items-center bg-blue-600 hover:bg-blue-700"
-            disabled={isSyncing || statusLoading}
-          >
-            {isSyncing ? (
-              <Loader2 className="w-4 h-4 animate-spin" />
-            ) : (
-              <Send className="w-4 h-4" />
-            )}
-            <span className="hidden sm:inline">Send to SHI</span>
-            {syncStatus?.pendingChanges > 0 && (
-              <Badge 
-                variant="destructive" 
-                className="h-5 w-5 p-0 text-xs flex items-center justify-center"
-              >
-                {syncStatus.pendingChanges}
-              </Badge>
-            )}
-          </Button>
-        </div>
+          <div className="flex items-center gap-3">
+            <Button
+              variant="default"
+              size="sm"
+              className="flex items-center bg-blue-600 hover:bg-blue-700"
+              disabled={isSyncing || totalStats.isLoading || documentsContractIds.length === 0}
+            >
+              {isSyncing ? (
+                <Loader2 className="w-4 h-4 animate-spin" />
+              ) : (
+                <Send className="w-4 h-4" />
+              )}
+              <span className="hidden sm:inline">Send to SHI</span>
+              {totalStats.totalPending > 0 && (
+                <Badge 
+                  variant="destructive" 
+                  className="h-5 w-5 p-0 text-xs flex items-center justify-center"
+                >
+                  {totalStats.totalPending}
+                </Badge>
+              )}
+            </Button>
+          </div>
         </PopoverTrigger>
         
-        <PopoverContent className="w-80">
+        <PopoverContent className="w-96">
           <div className="space-y-4">
             <div className="space-y-2">
               <h4 className="font-medium">SHI 동기화 상태</h4>
               <div className="flex items-center justify-between">
-                <span className="text-sm text-muted-foreground">현재 상태</span>
+                <span className="text-sm text-muted-foreground">전체 상태</span>
                 {getSyncStatusBadge()}
               </div>
+              <div className="text-xs text-muted-foreground">
+                {documentsContractIds.length}개 계약 대상
+              </div>
             </div>
 
-            {syncStatus && !statusError && (
+            {!totalStats.hasError && documentsContractIds.length > 0 && (
               <div className="space-y-3">
                 <Separator />
                 
-                <div className="grid grid-cols-2 gap-4 text-sm">
+                <div className="grid grid-cols-3 gap-4 text-sm">
                   <div>
                     <div className="text-muted-foreground">대기 중</div>
-                    <div className="font-medium">{syncStatus.pendingChanges || 0}건</div>
+                    <div className="font-medium">{totalStats.totalPending}건</div>
                   </div>
                   <div>
                     <div className="text-muted-foreground">동기화됨</div>
-                    <div className="font-medium">{syncStatus.syncedChanges || 0}건</div>
+                    <div className="font-medium">{totalStats.totalSynced}건</div>
                   </div>
-                </div>
-
-                {syncStatus.failedChanges > 0 && (
-                  <div className="text-sm">
+                  <div>
                     <div className="text-muted-foreground">실패</div>
-                    <div className="font-medium text-red-600">{syncStatus.failedChanges}건</div>
+                    <div className="font-medium text-red-600">{totalStats.totalFailed}건</div>
                   </div>
-                )}
+                </div>
 
-                {syncStatus.lastSyncAt && (
-                  <div className="text-sm">
-                    <div className="text-muted-foreground">마지막 동기화</div>
-                    <div className="font-medium">
-                      {new Date(syncStatus.lastSyncAt).toLocaleString()}
-                    </div>
+                {/* 계약별 상세 상태 */}
+                {contractStatuses.length > 1 && (
+                  <div className="space-y-2">
+                    <div className="text-sm font-medium">계약별 상태</div>
+                    <ScrollArea className="h-32">
+                      <div className="space-y-2">
+                        {contractStatuses.map(({ contractId, syncStatus, isLoading, error }) => (
+                          <div key={contractId} className="flex items-center justify-between text-xs p-2 rounded border">
+                            <span>Contract {contractId}</span>
+                            {isLoading ? (
+                              <Badge variant="secondary" className="text-xs">로딩...</Badge>
+                            ) : error ? (
+                              <Badge variant="destructive" className="text-xs">오류</Badge>
+                            ) : syncStatus?.pendingChanges > 0 ? (
+                              <Badge variant="destructive" className="text-xs">
+                                {syncStatus.pendingChanges}건 대기
+                              </Badge>
+                            ) : (
+                              <Badge variant="secondary" className="text-xs">동기화됨</Badge>
+                            )}
+                          </div>
+                        ))}
+                      </div>
+                    </ScrollArea>
                   </div>
                 )}
               </div>
             )}
 
-            {statusError && (
+            {totalStats.hasError && (
               <div className="space-y-2">
                 <Separator />
                 <div className="text-sm text-red-600">
                   <div className="font-medium">연결 오류</div>
-                  <div className="text-xs">동기화 상태를 확인할 수 없습니다.</div>
+                  <div className="text-xs">일부 계약의 동기화 상태를 확인할 수 없습니다.</div>
+                </div>
+              </div>
+            )}
+
+            {documentsContractIds.length === 0 && (
+              <div className="space-y-2">
+                <Separator />
+                <div className="text-sm text-muted-foreground">
+                  <div className="font-medium">계약 정보 없음</div>
+                  <div className="text-xs">동기화할 문서가 없습니다.</div>
                 </div>
               </div>
             )}
@@ -239,7 +365,7 @@ export function SendToSHIButton({
             <div className="flex gap-2">
               <Button
                 onClick={() => setIsDialogOpen(true)}
-                disabled={!canSync || isSyncing}
+                disabled={!totalStats.canSync || isSyncing}
                 className="flex-1"
                 size="sm"
               >
@@ -259,10 +385,10 @@ export function SendToSHIButton({
               <Button
                 variant="outline"
                 size="sm"
-                onClick={() => refetchStatus()}
-                disabled={statusLoading}
+                onClick={refreshAllStatuses}
+                disabled={totalStats.isLoading}
               >
-                {statusLoading ? (
+                {totalStats.isLoading ? (
                   <Loader2 className="w-4 h-4 animate-spin" />
                 ) : (
                   <Settings className="w-4 h-4" />
@@ -279,16 +405,21 @@ export function SendToSHIButton({
           <DialogHeader>
             <DialogTitle>SHI 시스템으로 동기화</DialogTitle>
             <DialogDescription>
-              변경된 문서 데이터를 SHI 시스템으로 전송합니다.
+              {documentsContractIds.length}개 계약의 변경된 문서 데이터를 SHI 시스템으로 전송합니다.
             </DialogDescription>
           </DialogHeader>
 
           <div className="space-y-4">
-            {syncStatus && !statusError && (
+            {!totalStats.hasError && documentsContractIds.length > 0 && (
               <div className="rounded-lg border p-4 space-y-3">
                 <div className="flex items-center justify-between text-sm">
                   <span>전송 대상</span>
-                  <span className="font-medium">{syncStatus.pendingChanges || 0}건</span>
+                  <span className="font-medium">{totalStats.totalPending}건</span>
+                </div>
+                
+                <div className="flex items-center justify-between text-sm">
+                  <span>대상 계약</span>
+                  <span className="font-medium">{documentsContractIds.length}개</span>
                 </div>
                 
                 <div className="text-xs text-muted-foreground">
@@ -299,18 +430,31 @@ export function SendToSHIButton({
                   <div className="space-y-2">
                     <div className="flex items-center justify-between text-sm">
                       <span>진행률</span>
-                      <span>{syncProgress}%</span>
+                      <span>{Math.round(syncProgress)}%</span>
                     </div>
                     <Progress value={syncProgress} className="h-2" />
+                    {currentSyncingContract && (
+                      <div className="text-xs text-muted-foreground">
+                        현재 처리 중: Contract {currentSyncingContract}
+                      </div>
+                    )}
                   </div>
                 )}
               </div>
             )}
 
-            {statusError && (
+            {totalStats.hasError && (
               <div className="rounded-lg border border-red-200 p-4">
                 <div className="text-sm text-red-600">
-                  동기화 상태를 확인할 수 없습니다. 네트워크 연결을 확인해주세요.
+                  일부 계약의 동기화 상태를 확인할 수 없습니다. 네트워크 연결을 확인해주세요.
+                </div>
+              </div>
+            )}
+
+            {documentsContractIds.length === 0 && (
+              <div className="rounded-lg border border-yellow-200 p-4">
+                <div className="text-sm text-yellow-700">
+                  동기화할 계약이 없습니다. 문서를 선택해주세요.
                 </div>
               </div>
             )}
@@ -325,7 +469,7 @@ export function SendToSHIButton({
               </Button>
               <Button
                 onClick={handleSync}
-                disabled={isSyncing || !canSync}
+                disabled={isSyncing || !totalStats.canSync}
               >
                 {isSyncing ? (
                   <>
-- 
cgit v1.2.3

