diff options
Diffstat (limited to 'lib/vendor-document-list')
| -rw-r--r-- | lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx | 32 | ||||
| -rw-r--r-- | lib/vendor-document-list/ship/send-to-shi-button.tsx | 336 |
2 files changed, 245 insertions, 123 deletions
diff --git a/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx b/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx index c3b7251c..255b1f9d 100644 --- a/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx +++ b/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx @@ -19,31 +19,11 @@ interface EnhancedDocTableToolbarActionsProps { export function EnhancedDocTableToolbarActions({ table, projectType, - contractId, }: EnhancedDocTableToolbarActionsProps) { const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = React.useState(false) // 현재 테이블의 모든 데이터 (필터링된 상태) const allDocuments = table.getFilteredRowModel().rows.map(row => row.original) - - // 모든 문서에서 고유한 contractId들 추출 - const contractIds = React.useMemo(() => { - const ids = new Set(allDocuments.map(doc => doc.contractId)) - return Array.from(ids) - }, [allDocuments]) - - // 주요 contractId (가장 많은 문서가 속한 계약) - const primaryContractId = React.useMemo(() => { - if (contractId) return contractId - if (contractIds.length === 0) return undefined - - const contractCounts = contractIds.map(id => ({ - id, - count: allDocuments.filter(doc => doc.contractId === id).length - })) - - return contractCounts.sort((a, b) => b.count - a.count)[0].id - }, [contractId, contractIds, allDocuments]) const handleSyncComplete = () => { // 동기화 완료 후 테이블 새로고침 @@ -98,13 +78,11 @@ export function EnhancedDocTableToolbarActions({ </Button> {/* Send to SHI 버튼 (공통) - 내부 → 외부로 보내기 */} - {primaryContractId && ( - <SendToSHIButton - contractId={primaryContractId} - onSyncComplete={handleSyncComplete} - projectType={projectType} - /> - )} + <SendToSHIButton + documents={allDocuments} + onSyncComplete={handleSyncComplete} + projectType={projectType} + /> </div> 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 ? ( <> |
