// components/sync/send-to-shi-button.tsx (최종 완성 버전) "use client" import * as React from "react" import { Send, Loader2, CheckCircle, AlertTriangle, Settings, RefreshCw } from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" 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 { Alert, AlertDescription } from "@/components/ui/alert" // ✅ 업데이트된 Hook import import { useClientSyncStatus, useTriggerSync, syncUtils } from "@/hooks/use-sync-status" import type { EnhancedDocument } from "@/types/enhanced-documents" import { useParams } from "next/navigation" import { useTranslation } from "@/i18n/client" import { useSession } from "next-auth/react" interface SendToSHIButtonProps { documents?: EnhancedDocument[] onSyncComplete?: () => void projectType: "ship" | "plant" } export function SendToSHIButton({ documents = [], onSyncComplete, projectType }: SendToSHIButtonProps) { const [isDialogOpen, setIsDialogOpen] = React.useState(false) const [syncProgress, setSyncProgress] = React.useState(0) const [currentSyncingContract, setCurrentSyncingContract] = React.useState(null) const { data: session } = useSession(); const params = useParams() const lng = (params?.lng as string) || "ko" const { t } = useTranslation(lng, "engineering") const targetSystem = projectType === 'ship' ? "DOLCE" : "SWP" // 문서에서 유효한 계약 ID 목록 추출 (projectId 사용) const documentsContractIds = React.useMemo(() => { const validIds = documents .map(doc => (doc as any).projectId) .filter((id): id is number => typeof id === 'number' && id > 0) const uniqueIds = [...new Set(validIds)] return uniqueIds.sort() }, [documents]) const vendorId = session?.user.companyId // ✅ 클라이언트 전용 Hook 사용 (서버 사이드 렌더링 호환) const { contractStatuses, totalStats, refetchAll } = useClientSyncStatus( documentsContractIds, targetSystem ) const { triggerSync, isLoading: isSyncing, error: syncError } = useTriggerSync() // 동기화 실행 함수 const handleSync = async () => { if (documentsContractIds.length === 0) { toast.info(t('shiSync.messages.noContractsToSync')) return } setSyncProgress(0) let successfulSyncs = 0 let failedSyncs = 0 let totalSuccessCount = 0 let totalFailureCount = 0 const errors: string[] = [] try { // 동기화 가능한 계약들만 필터링 const contractsToSync = contractStatuses.filter(({ syncStatus, error }) => { if (error) { console.warn(`Contract ${contractStatuses.find(c => c.error === error)?.projectId} 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(t('shiSync.messages.noPendingChanges')) setIsDialogOpen(false) return } console.log(`Starting sync for ${contractsToSync.length} contracts`) // 각 contract별로 순차 동기화 for (let i = 0; i < contractsToSync.length; i++) { const { projectId } = contractsToSync[i] setCurrentSyncingContract(projectId) try { console.log(`Syncing contract ${projectId}...`) const result = await triggerSync({ projectId, targetSystem }) if (result?.success) { successfulSyncs++ totalSuccessCount += result.successCount || 0 console.log(`Contract ${projectId} sync successful:`, result) } else { failedSyncs++ totalFailureCount += result?.failureCount || 0 const errorMsg = result?.errors?.[0] || result?.message || 'Unknown sync error' errors.push(t('shiSync.messages.contractError', { projectId, error: errorMsg })) console.error(`Contract ${projectId} sync failed:`, result) } } catch (error) { failedSyncs++ const errorMessage = error instanceof Error ? error.message : t('shiSync.messages.unknownError') errors.push(t('shiSync.messages.contractError', { projectId, error: errorMessage })) console.error(`Contract ${projectId} sync exception:`, error) } // 진행률 업데이트 setSyncProgress(((i + 1) / contractsToSync.length) * 100) } setCurrentSyncingContract(null) // 결과 처리 및 토스트 표시 setTimeout(() => { setSyncProgress(0) setIsDialogOpen(false) if (failedSyncs === 0) { toast.success( t('shiSync.messages.allSyncCompleted', { successCount: totalSuccessCount }), { description: t('shiSync.messages.allSyncCompletedDescription', { contractCount: successfulSyncs, itemCount: totalSuccessCount }) } ) } else if (successfulSyncs > 0) { toast.warning( t('shiSync.messages.partialSyncCompleted', { successfulCount: successfulSyncs, failedCount: failedSyncs }), { description: errors.slice(0, 3).join(', ') + (errors.length > 3 ? t('shiSync.messages.andMore') : '') } ) } else { toast.error( t('shiSync.messages.allSyncFailed', { failedCount: failedSyncs }), { description: errors[0] || t('shiSync.messages.allContractsSyncFailed') } ) } // 모든 contract 상태 갱신 refetchAll() onSyncComplete?.() }, 500) } catch (error) { setSyncProgress(0) setCurrentSyncingContract(null) const errorMessage = syncUtils.formatError(error as Error) toast.error(t('shiSync.messages.syncFailed'), { description: errorMessage }) console.error('Sync process failed:', error) } } // 동기화 상태에 따른 뱃지 생성 const getSyncStatusBadge = () => { if (totalStats.isLoading) { return ( {t('shiSync.status.checking')} ) } if (totalStats.hasError) { return ( {t('shiSync.status.connectionError')} ) } if (documentsContractIds.length === 0) { return {t('shiSync.status.noContracts')} } if (totalStats.totalPending > 0) { return ( {t('shiSync.status.pendingItems', { count: totalStats.totalPending })} ) } if (totalStats.totalSynced > 0) { return ( {t('shiSync.status.synchronized')} ) } return {t('shiSync.status.noChanges')} } return ( <>

{t('shiSync.labels.syncStatus')}

{t('shiSync.labels.overallStatus')} {getSyncStatusBadge()}
{t('shiSync.descriptions.targetInfo', { contractCount: documentsContractIds.length, targetSystem })}
{/* 에러 상태 표시 */} {totalStats.hasError && ( {t('shiSync.descriptions.statusCheckError')} {process.env.NODE_ENV === 'development' && (
Debug: {t('shiSync.descriptions.contractsWithError', { count: contractStatuses.filter(({ error }) => error).length })}
)}
)} {/* 정상 상태일 때 통계 표시 */} {!totalStats.hasError && documentsContractIds.length > 0 && (
{/* 전체 통계 */}
{t('shiSync.labels.pending')}
{t('shiSync.labels.itemCount', { count: totalStats.totalPending })}
{t('shiSync.labels.synced')}
{t('shiSync.labels.itemCount', { count: totalStats.totalSynced })}
{t('shiSync.labels.failed')}
{t('shiSync.labels.itemCount', { count: totalStats.totalFailed })}
{/* EntityType별 상세 통계 추가 */} {totalStats.entityTypeDetailsTotals && ( <>
{t('shiSync.labels.detailsByType')} {/* {t('shiSync.labels.experimental')} */}
{/* Document 통계 */} {totalStats.entityTypeDetailsTotals.document && (
{t('shiSync.labels.documents')}
{totalStats.entityTypeDetailsTotals.document.pending > 0 && ( {totalStats.entityTypeDetailsTotals.document.pending} {t('shiSync.labels.pendingShort')} )} {totalStats.entityTypeDetailsTotals.document.synced > 0 && ( {totalStats.entityTypeDetailsTotals.document.synced} {t('shiSync.labels.syncedShort')} )} {totalStats.entityTypeDetailsTotals.document.failed > 0 && ( {totalStats.entityTypeDetailsTotals.document.failed} {t('shiSync.labels.failedShort')} )}
)} {/* Revision 통계 */} {totalStats.entityTypeDetailsTotals.revision && (
{t('shiSync.labels.revisions')}
{totalStats.entityTypeDetailsTotals.revision.pending > 0 && ( {totalStats.entityTypeDetailsTotals.revision.pending} {t('shiSync.labels.pendingShort')} )} {totalStats.entityTypeDetailsTotals.revision.synced > 0 && ( {totalStats.entityTypeDetailsTotals.revision.synced} {t('shiSync.labels.syncedShort')} )} {totalStats.entityTypeDetailsTotals.revision.failed > 0 && ( {totalStats.entityTypeDetailsTotals.revision.failed} {t('shiSync.labels.failedShort')} )}
)} {/* Attachment 통계 */} {totalStats.entityTypeDetailsTotals.attachment && (
{t('shiSync.labels.attachments')}
{totalStats.entityTypeDetailsTotals.attachment.pending > 0 && ( {totalStats.entityTypeDetailsTotals.attachment.pending} {t('shiSync.labels.pendingShort')} )} {totalStats.entityTypeDetailsTotals.attachment.synced > 0 && ( {totalStats.entityTypeDetailsTotals.attachment.synced} {t('shiSync.labels.syncedShort')} )} {totalStats.entityTypeDetailsTotals.attachment.failed > 0 && ( {totalStats.entityTypeDetailsTotals.attachment.failed} {t('shiSync.labels.failedShort')} )}
)}
)} {/* 계약별 상세 상태 */} {contractStatuses.length > 1 && (
{t('shiSync.labels.statusByContract')}
{contractStatuses.map(({ projectId, syncStatus, isLoading, error }) => (
{t('shiSync.labels.contractLabel', { projectId })} {isLoading ? ( {t('shiSync.status.loading')} ) : error ? ( {t('shiSync.status.error')} ) : syncStatus && syncStatus.pendingChanges > 0 ? ( {t('shiSync.status.pendingCount', { count: syncStatus.pendingChanges })} ) : ( {t('shiSync.status.upToDate')} )}
))}
)}
)} {/* 계약 정보가 없는 경우 */} {documentsContractIds.length === 0 && ( {t('shiSync.descriptions.noDocumentsToSync')} )} {/* 액션 버튼들 */}
{/* 동기화 진행 다이얼로그 */} {t('shiSync.dialog.title')} {t('shiSync.dialog.description', { contractCount: documentsContractIds.length, targetSystem })}
{!totalStats.hasError && documentsContractIds.length > 0 && (
{t('shiSync.labels.syncTarget')} {t('shiSync.labels.itemCount', { count: totalStats.totalPending })}
{t('shiSync.labels.targetContracts')} {t('shiSync.labels.contractCount', { count: documentsContractIds.length })}
{t('shiSync.descriptions.includesChanges')}
{isSyncing && (
{t('shiSync.labels.progress')} {Math.round(syncProgress)}%
{currentSyncingContract && (
{t('shiSync.descriptions.currentlyProcessing', { contractId: currentSyncingContract })}
)}
)}
)} {totalStats.hasError && ( {t('shiSync.descriptions.dialogStatusCheckError')} )} {documentsContractIds.length === 0 && ( {t('shiSync.descriptions.noContractsToSync')} )}
) }