summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/ship/send-to-shi-button.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-22 02:57:00 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-22 02:57:00 +0000
commitee57cc221ff2edafd3c0f12a181214c602ed257e (patch)
tree148f552f503798f7a350d6eff936b889f16be49f /lib/vendor-document-list/ship/send-to-shi-button.tsx
parent14f61e24947fb92dd71ec0a7196a6e815f8e66da (diff)
(대표님, 최겸) 이메일 템플릿, 벤더데이터 변경사항 대응, 기술영업 변경요구사항 구현
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.tsx286
1 files changed, 156 insertions, 130 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 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">