summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/ship/send-to-shi-button.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list/ship/send-to-shi-button.tsx')
-rw-r--r--lib/vendor-document-list/ship/send-to-shi-button.tsx336
1 files changed, 240 insertions, 96 deletions
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 ? (
<>