From ee57cc221ff2edafd3c0f12a181214c602ed257e Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Tue, 22 Jul 2025 02:57:00 +0000
Subject: (대표님, 최겸) 이메일 템플릿, 벤더데이터 변경사항 대응, 기술영업
 변경요구사항 구현
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../ship/send-to-shi-button.tsx                    | 286 +++++++++++----------
 1 file changed, 156 insertions(+), 130 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 61893da5..4607c994 100644
--- a/lib/vendor-document-list/ship/send-to-shi-button.tsx
+++ b/lib/vendor-document-list/ship/send-to-shi-button.tsx
@@ -1,8 +1,8 @@
-// components/sync/send-to-shi-button.tsx (다중 계약 버전)
+// components/sync/send-to-shi-button.tsx (최종 완성 버전)
 "use client"
 
 import * as React from "react"
-import { Send, Loader2, CheckCircle, AlertTriangle, Settings } from "lucide-react"
+import { Send, Loader2, CheckCircle, AlertTriangle, Settings, RefreshCw } from "lucide-react"
 import { toast } from "sonner"
 
 import { Button } from "@/components/ui/button"
@@ -22,7 +22,9 @@ 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 { Alert, AlertDescription } from "@/components/ui/alert"
+// ✅ 업데이트된 Hook import
+import { useClientSyncStatus, useTriggerSync, syncUtils } from "@/hooks/use-sync-status"
 import type { EnhancedDocument } from "@/types/enhanced-documents"
 
 interface SendToSHIButtonProps {
@@ -31,13 +33,6 @@ interface SendToSHIButtonProps {
   projectType: "ship" | "plant"
 }
 
-interface ContractSyncStatus {
-  contractId: number
-  syncStatus: any
-  isLoading: boolean
-  error: any
-}
-
 export function SendToSHIButton({ 
   documents = [], 
   onSyncComplete,
@@ -49,75 +44,45 @@ export function SendToSHIButton({
 
   const targetSystem = projectType === 'ship' ? "DOLCE" : "SWP"
   
-  // documents에서 contractId 목록 추출
+  // 문서에서 유효한 계약 ID 목록 추출
   const documentsContractIds = React.useMemo(() => {
-    const uniqueIds = [...new Set(documents.map(doc => doc.contractId).filter(Boolean))]
+    const validIds = documents
+      .map(doc => doc.contractId)
+      .filter((id): id is number => typeof id === 'number' && id > 0)
+    
+    const uniqueIds = [...new Set(validIds)]
     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, 
-    isLoading: isSyncing, 
-    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])
+  // ✅ 클라이언트 전용 Hook 사용 (서버 사이드 렌더링 호환)
+  const { contractStatuses, totalStats, refetchAll } = useClientSyncStatus(
+    documentsContractIds, 
+    targetSystem
+  )
+
+  const { triggerSync, isLoading: isSyncing, error: syncError } = useTriggerSync()
 
-  // 에러 상태 표시
+  // 개발 환경에서 디버깅 정보
   React.useEffect(() => {
-    if (totalStats.hasError) {
-      console.warn('Failed to load sync status for some contracts')
+    if (process.env.NODE_ENV === 'development') {
+      console.log('SendToSHIButton Debug Info:', {
+        documentsContractIds,
+        totalStats,
+        contractStatuses: contractStatuses.map(({ contractId, syncStatus, error }) => ({
+          contractId,
+          pendingChanges: syncStatus?.pendingChanges,
+          hasError: !!error
+        }))
+      })
     }
-  }, [totalStats.hasError])
+  }, [documentsContractIds, totalStats, contractStatuses])
 
+  // 동기화 실행 함수
   const handleSync = async () => {
-    if (documentsContractIds.length === 0) return
+    if (documentsContractIds.length === 0) {
+      toast.info('동기화할 계약이 없습니다.')
+      return
+    }
     
     setSyncProgress(0)
     let successfulSyncs = 0
@@ -127,9 +92,17 @@ export function SendToSHIButton({
     const errors: string[] = []
     
     try {
-      const contractsToSync = contractStatuses.filter(
-        ({ syncStatus, error }) => !error && syncStatus?.syncEnabled && syncStatus?.pendingChanges > 0
-      )
+      // 동기화 가능한 계약들만 필터링
+      const contractsToSync = contractStatuses.filter(({ syncStatus, error }) => {
+        if (error) {
+          console.warn(`Contract ${contractStatuses.find(c => c.error === error)?.contractId} has error:`, error)
+          return false
+        }
+        if (!syncStatus) return false
+        if (!syncStatus.syncEnabled) return false
+        if (syncStatus.pendingChanges <= 0) return false
+        return true
+      })
 
       if (contractsToSync.length === 0) {
         toast.info('동기화할 변경사항이 없습니다.')
@@ -137,12 +110,15 @@ export function SendToSHIButton({
         return
       }
 
+      console.log(`Starting sync for ${contractsToSync.length} contracts`)
+
       // 각 contract별로 순차 동기화
       for (let i = 0; i < contractsToSync.length; i++) {
         const { contractId } = contractsToSync[i]
         setCurrentSyncingContract(contractId)
         
         try {
+          console.log(`Syncing contract ${contractId}...`)
           const result = await triggerSync({ 
             contractId, 
             targetSystem 
@@ -151,17 +127,19 @@ export function SendToSHIButton({
           if (result?.success) {
             successfulSyncs++
             totalSuccessCount += result.successCount || 0
+            console.log(`Contract ${contractId} sync successful:`, result)
           } else {
             failedSyncs++
             totalFailureCount += result?.failureCount || 0
-            if (result?.errors?.[0]) {
-              errors.push(`Contract ${contractId}: ${result.errors[0]}`)
-            }
+            const errorMsg = result?.errors?.[0] || result?.message || 'Unknown sync error'
+            errors.push(`Contract ${contractId}: ${errorMsg}`)
+            console.error(`Contract ${contractId} sync failed:`, result)
           }
         } catch (error) {
           failedSyncs++
           const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류'
           errors.push(`Contract ${contractId}: ${errorMessage}`)
+          console.error(`Contract ${contractId} sync exception:`, error)
         }
 
         // 진행률 업데이트
@@ -170,6 +148,7 @@ export function SendToSHIButton({
 
       setCurrentSyncingContract(null)
       
+      // 결과 처리 및 토스트 표시
       setTimeout(() => {
         setSyncProgress(0)
         setIsDialogOpen(false)
@@ -185,7 +164,7 @@ export function SendToSHIButton({
           toast.warning(
             `부분 동기화 완료: ${successfulSyncs}개 성공, ${failedSyncs}개 실패`,
             {
-              description: errors[0] || '일부 계약 동기화에 실패했습니다.'
+              description: errors.slice(0, 3).join(', ') + (errors.length > 3 ? ' 외 더보기...' : '')
             }
           )
         } else {
@@ -198,7 +177,7 @@ export function SendToSHIButton({
         }
         
         // 모든 contract 상태 갱신
-        contractStatuses.forEach(({ refetch }) => refetch?.())
+        refetchAll()
         onSyncComplete?.()
       }, 500)
       
@@ -206,19 +185,32 @@ export function SendToSHIButton({
       setSyncProgress(0)
       setCurrentSyncingContract(null)
       
+      const errorMessage = syncUtils.formatError(error as any)
       toast.error('동기화 실패', {
-        description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.'
+        description: errorMessage
       })
+      console.error('Sync process failed:', error)
     }
   }
 
+  // 동기화 상태에 따른 뱃지 생성
   const getSyncStatusBadge = () => {
     if (totalStats.isLoading) {
-      return <Badge variant="secondary">확인 중...</Badge>
+      return (
+        <Badge variant="secondary" className="gap-1">
+          <Loader2 className="w-3 h-3 animate-spin" />
+          확인 중...
+        </Badge>
+      )
     }
 
     if (totalStats.hasError) {
-      return <Badge variant="destructive">오류</Badge>
+      return (
+        <Badge variant="destructive" className="gap-1">
+          <AlertTriangle className="w-3 h-3" />
+          연결 오류
+        </Badge>
+      )
     }
 
     if (documentsContractIds.length === 0) {
@@ -246,10 +238,6 @@ export function SendToSHIButton({
     return <Badge variant="secondary">변경사항 없음</Badge>
   }
 
-  const refreshAllStatuses = () => {
-    contractStatuses.forEach(({ refetch }) => refetch?.())
-  }
-
   return (
     <>
       <Popover>
@@ -258,7 +246,7 @@ export function SendToSHIButton({
             <Button
               variant="default"
               size="sm"
-              className="flex items-center bg-blue-600 hover:bg-blue-700"
+              className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700"
               disabled={isSyncing || totalStats.isLoading || documentsContractIds.length === 0}
             >
               {isSyncing ? (
@@ -270,7 +258,7 @@ export function SendToSHIButton({
               {totalStats.totalPending > 0 && (
                 <Badge 
                   variant="destructive" 
-                  className="h-5 w-5 p-0 text-xs flex items-center justify-center"
+                  className="h-5 w-5 p-0 text-xs flex items-center justify-center ml-1"
                 >
                   {totalStats.totalPending}
                 </Badge>
@@ -279,33 +267,66 @@ export function SendToSHIButton({
           </div>
         </PopoverTrigger>
         
-        <PopoverContent className="w-96">
+        <PopoverContent className="w-96" align="end">
           <div className="space-y-4">
             <div className="space-y-2">
-              <h4 className="font-medium">SHI 동기화 상태</h4>
+              <div className="flex items-center justify-between">
+                <h4 className="font-medium">SHI 동기화 상태</h4>
+                <Button
+                  variant="ghost"
+                  size="sm"
+                  onClick={refetchAll}
+                  disabled={totalStats.isLoading}
+                  className="h-6 w-6 p-0"
+                >
+                  {totalStats.isLoading ? (
+                    <Loader2 className="w-3 h-3 animate-spin" />
+                  ) : (
+                    <RefreshCw className="w-3 h-3" />
+                  )}
+                </Button>
+              </div>
+              
               <div className="flex items-center justify-between">
                 <span className="text-sm text-muted-foreground">전체 상태</span>
                 {getSyncStatusBadge()}
               </div>
+              
               <div className="text-xs text-muted-foreground">
-                {documentsContractIds.length}개 계약 대상
+                {documentsContractIds.length}개 계약 대상 • {targetSystem} 시스템
               </div>
             </div>
 
+            {/* 에러 상태 표시 */}
+            {totalStats.hasError && (
+              <Alert variant="destructive">
+                <AlertTriangle className="h-4 w-4" />
+                <AlertDescription>
+                  일부 계약의 동기화 상태를 확인할 수 없습니다. 네트워크 연결을 확인해주세요.
+                  {process.env.NODE_ENV === 'development' && (
+                    <div className="text-xs mt-1 font-mono">
+                      Debug: {contractStatuses.filter(({ error }) => error).length}개 계약에서 오류
+                    </div>
+                  )}
+                </AlertDescription>
+              </Alert>
+            )}
+
+            {/* 정상 상태일 때 통계 표시 */}
             {!totalStats.hasError && documentsContractIds.length > 0 && (
               <div className="space-y-3">
                 <Separator />
                 
                 <div className="grid grid-cols-3 gap-4 text-sm">
-                  <div>
+                  <div className="text-center">
                     <div className="text-muted-foreground">대기 중</div>
-                    <div className="font-medium">{totalStats.totalPending}건</div>
+                    <div className="font-medium text-orange-600">{totalStats.totalPending}건</div>
                   </div>
-                  <div>
+                  <div className="text-center">
                     <div className="text-muted-foreground">동기화됨</div>
-                    <div className="font-medium">{totalStats.totalSynced}건</div>
+                    <div className="font-medium text-green-600">{totalStats.totalSynced}건</div>
                   </div>
-                  <div>
+                  <div className="text-center">
                     <div className="text-muted-foreground">실패</div>
                     <div className="font-medium text-red-600">{totalStats.totalFailed}건</div>
                   </div>
@@ -319,17 +340,26 @@ export function SendToSHIButton({
                       <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>
+                            <span className="font-medium">Contract {contractId}</span>
                             {isLoading ? (
-                              <Badge variant="secondary" className="text-xs">로딩...</Badge>
+                              <Badge variant="secondary" className="text-xs">
+                                <Loader2 className="w-3 h-3 mr-1 animate-spin" />
+                                로딩...
+                              </Badge>
                             ) : error ? (
-                              <Badge variant="destructive" className="text-xs">오류</Badge>
-                            ) : syncStatus?.pendingChanges > 0 ? (
+                              <Badge variant="destructive" className="text-xs">
+                                <AlertTriangle className="w-3 h-3 mr-1" />
+                                오류
+                              </Badge>
+                            ) : syncStatus && syncStatus.pendingChanges > 0 ? (
                               <Badge variant="destructive" className="text-xs">
                                 {syncStatus.pendingChanges}건 대기
                               </Badge>
                             ) : (
-                              <Badge variant="secondary" className="text-xs">동기화됨</Badge>
+                              <Badge variant="secondary" className="text-xs">
+                                <CheckCircle className="w-3 h-3 mr-1" />
+                                최신
+                              </Badge>
                             )}
                           </div>
                         ))}
@@ -340,28 +370,18 @@ export function SendToSHIButton({
               </div>
             )}
 
-            {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>
-              </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>
+              <Alert>
+                <AlertDescription>
+                  동기화할 문서가 없습니다. 문서를 선택해주세요.
+                </AlertDescription>
+              </Alert>
             )}
 
             <Separator />
 
+            {/* 액션 버튼들 */}
             <div className="flex gap-2">
               <Button
                 onClick={() => setIsDialogOpen(true)}
@@ -385,8 +405,9 @@ export function SendToSHIButton({
               <Button
                 variant="outline"
                 size="sm"
-                onClick={refreshAllStatuses}
+                onClick={refetchAll}
                 disabled={totalStats.isLoading}
+                className="px-3"
               >
                 {totalStats.isLoading ? (
                   <Loader2 className="w-4 h-4 animate-spin" />
@@ -403,9 +424,12 @@ export function SendToSHIButton({
       <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
         <DialogContent className="sm:max-w-md">
           <DialogHeader>
-            <DialogTitle>SHI 시스템으로 동기화</DialogTitle>
+            <DialogTitle className="flex items-center gap-2">
+              <Send className="w-5 h-5" />
+              SHI 시스템으로 동기화
+            </DialogTitle>
             <DialogDescription>
-              {documentsContractIds.length}개 계약의 변경된 문서 데이터를 SHI 시스템으로 전송합니다.
+              {documentsContractIds.length}개 계약의 변경된 문서 데이터를 {targetSystem} 시스템으로 전송합니다.
             </DialogDescription>
           </DialogHeader>
 
@@ -434,7 +458,8 @@ export function SendToSHIButton({
                     </div>
                     <Progress value={syncProgress} className="h-2" />
                     {currentSyncingContract && (
-                      <div className="text-xs text-muted-foreground">
+                      <div className="text-xs text-muted-foreground flex items-center gap-1">
+                        <Loader2 className="w-3 h-3 animate-spin" />
                         현재 처리 중: Contract {currentSyncingContract}
                       </div>
                     )}
@@ -444,19 +469,20 @@ export function SendToSHIButton({
             )}
 
             {totalStats.hasError && (
-              <div className="rounded-lg border border-red-200 p-4">
-                <div className="text-sm text-red-600">
-                  일부 계약의 동기화 상태를 확인할 수 없습니다. 네트워크 연결을 확인해주세요.
-                </div>
-              </div>
+              <Alert variant="destructive">
+                <AlertTriangle className="h-4 w-4" />
+                <AlertDescription>
+                  일부 계약의 동기화 상태를 확인할 수 없습니다. 네트워크 연결을 확인하고 다시 시도해주세요.
+                </AlertDescription>
+              </Alert>
             )}
 
             {documentsContractIds.length === 0 && (
-              <div className="rounded-lg border border-yellow-200 p-4">
-                <div className="text-sm text-yellow-700">
+              <Alert>
+                <AlertDescription>
                   동기화할 계약이 없습니다. 문서를 선택해주세요.
-                </div>
-              </div>
+                </AlertDescription>
+              </Alert>
             )}
 
             <div className="flex justify-end gap-2">
-- 
cgit v1.2.3

