summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list/table')
-rw-r--r--lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx57
-rw-r--r--lib/vendor-document-list/table/enhanced-documents-table.tsx2
-rw-r--r--lib/vendor-document-list/table/import-from-dolce-button.tsx356
-rw-r--r--lib/vendor-document-list/table/swp-workflow-panel.tsx370
4 files changed, 770 insertions, 15 deletions
diff --git a/lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx b/lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx
index fa1b957b..3960bbce 100644
--- a/lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx
+++ b/lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx
@@ -1,8 +1,7 @@
"use client"
-
import * as React from "react"
import { type Table } from "@tanstack/react-table"
-import { Download, Upload, Plus, Files } from "lucide-react"
+import { Download, Upload, Plus, Files, RefreshCw } from "lucide-react"
import { toast } from "sonner"
import { exportTableToExcel } from "@/lib/export"
@@ -13,6 +12,8 @@ import { DeleteDocumentsDialog } from "./delete-docs-dialog"
import { BulkUploadDialog } from "./bulk-upload-dialog"
import type { EnhancedDocument } from "@/types/enhanced-documents"
import { SendToSHIButton } from "./send-to-shi-button"
+import { ImportFromDOLCEButton } from "./import-from-dolce-button"
+import { SWPWorkflowPanel } from "./swp-workflow-panel"
interface EnhancedDocTableToolbarActionsProps {
table: Table<EnhancedDocument>
@@ -50,9 +51,17 @@ export function EnhancedDocTableToolbarActions({
}, 500)
}
+ const handleImportComplete = () => {
+ // 가져오기 완료 후 테이블 새로고침
+ table.resetRowSelection()
+ setTimeout(() => {
+ window.location.reload()
+ }, 500)
+ }
+
return (
<div className="flex items-center gap-2">
- {/* 기존 액션들 */}
+ {/* 삭제 버튼 */}
{table.getFilteredSelectedRowModel().rows.length > 0 ? (
<DeleteDocumentsDialog
documents={table
@@ -62,14 +71,27 @@ export function EnhancedDocTableToolbarActions({
/>
) : null}
- {/* ✅ AddDocumentListDialog에 필요한 props 전달 */}
- <AddDocumentListDialog
- projectType={projectType}
- contractId={selectedPackageId}
- onSuccess={handleDocumentAdded} // ✅ 성공 콜백 추가
- />
-
- {/* 일괄 업로드 버튼 */}
+ {/* projectType에 따른 조건부 렌더링 */}
+ {projectType === "ship" ? (
+ <>
+ {/* SHIP: DOLCE에서 목록 가져오기 */}
+ <ImportFromDOLCEButton
+ contractId={selectedPackageId}
+ onImportComplete={handleImportComplete}
+ />
+ </>
+ ) : (
+ <>
+ {/* PLANT: 수동 문서 추가 */}
+ <AddDocumentListDialog
+ projectType={projectType}
+ contractId={selectedPackageId}
+ onSuccess={handleDocumentAdded}
+ />
+ </>
+ )}
+
+ {/* 일괄 업로드 버튼 (공통) */}
<Button
variant="outline"
onClick={() => setBulkUploadDialogOpen(true)}
@@ -79,7 +101,7 @@ export function EnhancedDocTableToolbarActions({
일괄 업로드
</Button>
- {/* Export 버튼 */}
+ {/* Export 버튼 (공통) */}
<Button
variant="outline"
size="sm"
@@ -95,7 +117,7 @@ export function EnhancedDocTableToolbarActions({
<span className="hidden sm:inline">Export</span>
</Button>
- {/* ✅ 새로운 Send to SHI 버튼으로 교체 */}
+ {/* Send to SHI 버튼 (공통) - 내부 → 외부로 보내기 */}
<SendToSHIButton
contractId={selectedPackageId}
documents={allDocuments}
@@ -103,6 +125,15 @@ export function EnhancedDocTableToolbarActions({
projectType={projectType}
/>
+ {/* SWP 전용 워크플로우 패널 */}
+ {projectType === "plant" && (
+ <SWPWorkflowPanel
+ contractId={selectedPackageId}
+ documents={allDocuments}
+ onWorkflowUpdate={handleSyncComplete}
+ />
+ )}
+
{/* 일괄 업로드 다이얼로그 */}
<BulkUploadDialog
open={bulkUploadDialogOpen}
diff --git a/lib/vendor-document-list/table/enhanced-documents-table.tsx b/lib/vendor-document-list/table/enhanced-documents-table.tsx
index f840a10c..3bd6668d 100644
--- a/lib/vendor-document-list/table/enhanced-documents-table.tsx
+++ b/lib/vendor-document-list/table/enhanced-documents-table.tsx
@@ -46,8 +46,6 @@ export function EnhancedDocumentsTable({
// 데이터 로딩
const [{ data, pageCount, total }] = React.use(promises)
- console.log(data)
-
// 상태 관리
const [rowAction, setRowAction] = React.useState<DataTableRowAction<EnhancedDocument> | null>(null)
diff --git a/lib/vendor-document-list/table/import-from-dolce-button.tsx b/lib/vendor-document-list/table/import-from-dolce-button.tsx
new file mode 100644
index 00000000..519d40cb
--- /dev/null
+++ b/lib/vendor-document-list/table/import-from-dolce-button.tsx
@@ -0,0 +1,356 @@
+"use client"
+
+import * as React from "react"
+import { RefreshCw, Download, Loader2, CheckCircle, AlertTriangle } 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"
+
+interface ImportFromDOLCEButtonProps {
+ contractId: number
+ onImportComplete?: () => void
+}
+
+interface ImportStatus {
+ lastImportAt?: string
+ availableDocuments: number
+ newDocuments: number
+ updatedDocuments: number
+ importEnabled: boolean
+}
+
+export function ImportFromDOLCEButton({
+ contractId,
+ onImportComplete
+}: ImportFromDOLCEButtonProps) {
+ const [isDialogOpen, setIsDialogOpen] = React.useState(false)
+ const [importProgress, setImportProgress] = React.useState(0)
+ const [isImporting, setIsImporting] = React.useState(false)
+ const [importStatus, setImportStatus] = React.useState<ImportStatus | null>(null)
+ const [statusLoading, setStatusLoading] = React.useState(false)
+
+ // DOLCE 상태 조회
+ const fetchImportStatus = async () => {
+ setStatusLoading(true)
+ try {
+ const response = await fetch(`/api/sync/import/status?contractId=${contractId}&sourceSystem=DOLCE`)
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}))
+ throw new Error(errorData.message || 'Failed to fetch import status')
+ }
+
+ const status = await response.json()
+ setImportStatus(status)
+
+ // 프로젝트 코드가 없는 경우 에러 처리
+ if (status.error) {
+ toast.error(`상태 확인 실패: ${status.error}`)
+ setImportStatus(null)
+ }
+ } catch (error) {
+ console.error('Failed to fetch import status:', error)
+ toast.error('DOLCE 상태를 확인할 수 없습니다. 프로젝트 설정을 확인해주세요.')
+ setImportStatus(null)
+ } finally {
+ setStatusLoading(false)
+ }
+ }
+
+ // 컴포넌트 마운트 시 상태 조회
+ React.useEffect(() => {
+ fetchImportStatus()
+ }, [contractId])
+
+ const handleImport = async () => {
+ if (!contractId) return
+
+ setImportProgress(0)
+ setIsImporting(true)
+
+ try {
+ // 진행률 시뮬레이션
+ const progressInterval = setInterval(() => {
+ setImportProgress(prev => Math.min(prev + 15, 90))
+ }, 300)
+
+ const response = await fetch('/api/sync/import', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ contractId,
+ sourceSystem: 'DOLCE'
+ })
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json()
+ throw new Error(errorData.message || 'Import failed')
+ }
+
+ const result = await response.json()
+
+ clearInterval(progressInterval)
+ setImportProgress(100)
+
+ setTimeout(() => {
+ setImportProgress(0)
+ setIsDialogOpen(false)
+ setIsImporting(false)
+
+ if (result?.success) {
+ const { newCount = 0, updatedCount = 0, skippedCount = 0 } = result
+ toast.success(
+ `DOLCE 가져오기 완료`,
+ {
+ description: `신규 ${newCount}건, 업데이트 ${updatedCount}건, 건너뜀 ${skippedCount}건 (B3/B4/B5 포함)`
+ }
+ )
+ } else {
+ toast.error(
+ `DOLCE 가져오기 부분 실패`,
+ {
+ description: result?.message || '일부 DrawingKind에서 가져오기에 실패했습니다.'
+ }
+ )
+ }
+
+ fetchImportStatus() // 상태 갱신
+ onImportComplete?.()
+ }, 500)
+
+ } catch (error) {
+ setImportProgress(0)
+ setIsImporting(false)
+
+ toast.error('DOLCE 가져오기 실패', {
+ description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.'
+ })
+ }
+ }
+
+ const getStatusBadge = () => {
+ if (statusLoading) {
+ return <Badge variant="secondary">DOLCE 연결 확인 중...</Badge>
+ }
+
+ if (!importStatus) {
+ return <Badge variant="destructive">DOLCE 연결 오류</Badge>
+ }
+
+ if (!importStatus.importEnabled) {
+ return <Badge variant="secondary">DOLCE 가져오기 비활성화</Badge>
+ }
+
+ if (importStatus.newDocuments > 0 || importStatus.updatedDocuments > 0) {
+ return (
+ <Badge variant="default" className="gap-1 bg-blue-500 hover:bg-blue-600">
+ <AlertTriangle className="w-3 h-3" />
+ 업데이트 가능 (B3/B4/B5)
+ </Badge>
+ )
+ }
+
+ return (
+ <Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600">
+ <CheckCircle className="w-3 h-3" />
+ DOLCE와 동기화됨
+ </Badge>
+ )
+ }
+
+ const canImport = importStatus?.importEnabled &&
+ (importStatus?.newDocuments > 0 || importStatus?.updatedDocuments > 0)
+
+ return (
+ <>
+ <Popover>
+ <PopoverTrigger asChild>
+ <div className="flex items-center gap-3">
+ <Button
+ variant="outline"
+ size="sm"
+ className="flex items-center border-blue-200 hover:bg-blue-50"
+ disabled={isImporting || statusLoading}
+ >
+ {isImporting ? (
+ <Loader2 className="w-4 h-4 animate-spin" />
+ ) : (
+ <Download className="w-4 h-4" />
+ )}
+ <span className="hidden sm:inline">DOLCE에서 가져오기</span>
+ {importStatus && (importStatus.newDocuments > 0 || importStatus.updatedDocuments > 0) && (
+ <Badge
+ variant="default"
+ className="h-5 w-5 p-0 text-xs flex items-center justify-center bg-blue-500"
+ >
+ {importStatus.newDocuments + importStatus.updatedDocuments}
+ </Badge>
+ )}
+ </Button>
+ </div>
+ </PopoverTrigger>
+
+ <PopoverContent className="w-80">
+ <div className="space-y-4">
+ <div className="space-y-2">
+ <h4 className="font-medium">DOLCE 가져오기 상태</h4>
+ <div className="flex items-center justify-between">
+ <span className="text-sm text-muted-foreground">현재 상태</span>
+ {getStatusBadge()}
+ </div>
+ </div>
+
+ {importStatus && (
+ <div className="space-y-3">
+ <Separator />
+
+ <div className="grid grid-cols-2 gap-4 text-sm">
+ <div>
+ <div className="text-muted-foreground">신규 문서</div>
+ <div className="font-medium">{importStatus.newDocuments || 0}건</div>
+ </div>
+ <div>
+ <div className="text-muted-foreground">업데이트</div>
+ <div className="font-medium">{importStatus.updatedDocuments || 0}건</div>
+ </div>
+ </div>
+
+ <div className="text-sm">
+ <div className="text-muted-foreground">DOLCE 전체 문서 (B3/B4/B5)</div>
+ <div className="font-medium">{importStatus.availableDocuments || 0}건</div>
+ </div>
+
+ {importStatus.lastImportAt && (
+ <div className="text-sm">
+ <div className="text-muted-foreground">마지막 가져오기</div>
+ <div className="font-medium">
+ {new Date(importStatus.lastImportAt).toLocaleString()}
+ </div>
+ </div>
+ )}
+ </div>
+ )}
+
+ <Separator />
+
+ <div className="flex gap-2">
+ <Button
+ onClick={() => setIsDialogOpen(true)}
+ disabled={!canImport || isImporting}
+ className="flex-1"
+ size="sm"
+ >
+ {isImporting ? (
+ <>
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+ 가져오는 중...
+ </>
+ ) : (
+ <>
+ <Download className="w-4 h-4 mr-2" />
+ 지금 가져오기
+ </>
+ )}
+ </Button>
+
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={fetchImportStatus}
+ disabled={statusLoading}
+ >
+ {statusLoading ? (
+ <Loader2 className="w-4 h-4 animate-spin" />
+ ) : (
+ <RefreshCw className="w-4 h-4" />
+ )}
+ </Button>
+ </div>
+ </div>
+ </PopoverContent>
+ </Popover>
+
+ {/* 가져오기 진행 다이얼로그 */}
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
+ <DialogContent className="sm:max-w-md">
+ <DialogHeader>
+ <DialogTitle>DOLCE에서 문서 목록 가져오기</DialogTitle>
+ <DialogDescription>
+ 삼성중공업 DOLCE 시스템에서 최신 문서 목록을 가져옵니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ {importStatus && (
+ <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">
+ {(importStatus.newDocuments || 0) + (importStatus.updatedDocuments || 0)}건
+ </span>
+ </div>
+
+ <div className="text-xs text-muted-foreground">
+ 신규 문서와 업데이트된 문서가 포함됩니다. (B3, B4, B5)
+ <br />
+ B4 문서의 경우 GTTPreDwg, GTTWorkingDwg 이슈 스테이지가 자동 생성됩니다.
+ </div>
+
+ {isImporting && (
+ <div className="space-y-2">
+ <div className="flex items-center justify-between text-sm">
+ <span>진행률</span>
+ <span>{importProgress}%</span>
+ </div>
+ <Progress value={importProgress} className="h-2" />
+ </div>
+ )}
+ </div>
+ )}
+
+ <div className="flex justify-end gap-2">
+ <Button
+ variant="outline"
+ onClick={() => setIsDialogOpen(false)}
+ disabled={isImporting}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleImport}
+ disabled={isImporting || !canImport}
+ >
+ {isImporting ? (
+ <>
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+ 가져오는 중...
+ </>
+ ) : (
+ <>
+ <Download className="w-4 h-4 mr-2" />
+ 가져오기 시작
+ </>
+ )}
+ </Button>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ </>
+ )
+} \ No newline at end of file
diff --git a/lib/vendor-document-list/table/swp-workflow-panel.tsx b/lib/vendor-document-list/table/swp-workflow-panel.tsx
new file mode 100644
index 00000000..ded306e7
--- /dev/null
+++ b/lib/vendor-document-list/table/swp-workflow-panel.tsx
@@ -0,0 +1,370 @@
+"use client"
+
+import * as React from "react"
+import { Send, Eye, CheckCircle, Clock, RefreshCw, AlertTriangle, Loader2 } from "lucide-react"
+import { toast } from "sonner"
+
+import { Button } from "@/components/ui/button"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+import { Badge } from "@/components/ui/badge"
+import { Separator } from "@/components/ui/separator"
+import { Progress } from "@/components/ui/progress"
+import type { EnhancedDocument } from "@/types/enhanced-documents"
+
+interface SWPWorkflowPanelProps {
+ contractId: number
+ documents: EnhancedDocument[]
+ onWorkflowUpdate?: () => void
+}
+
+type WorkflowStatus =
+ | 'IDLE' // 대기 상태
+ | 'SUBMITTED' // 목록 전송됨
+ | 'UNDER_REVIEW' // 검토 중
+ | 'CONFIRMED' // 컨펌됨
+ | 'REVISION_REQUIRED' // 수정 요청됨
+ | 'RESUBMITTED' // 재전송됨
+ | 'APPROVED' // 최종 승인됨
+
+interface WorkflowState {
+ status: WorkflowStatus
+ lastUpdatedAt?: string
+ pendingActions: string[]
+ confirmationData?: any
+ revisionComments?: string[]
+ approvalData?: any
+}
+
+export function SWPWorkflowPanel({
+ contractId,
+ documents,
+ onWorkflowUpdate
+}: SWPWorkflowPanelProps) {
+ const [workflowState, setWorkflowState] = React.useState<WorkflowState | null>(null)
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [actionProgress, setActionProgress] = React.useState(0)
+
+ // 워크플로우 상태 조회
+ const fetchWorkflowStatus = async () => {
+ setIsLoading(true)
+ try {
+ const response = await fetch(`/api/sync/workflow/status?contractId=${contractId}&targetSystem=SWP`)
+ if (!response.ok) throw new Error('Failed to fetch workflow status')
+
+ const status = await response.json()
+ setWorkflowState(status)
+ } catch (error) {
+ console.error('Failed to fetch workflow status:', error)
+ toast.error('워크플로우 상태를 확인할 수 없습니다')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ // 컴포넌트 마운트 시 상태 조회
+ React.useEffect(() => {
+ fetchWorkflowStatus()
+ }, [contractId])
+
+ // 워크플로우 액션 실행
+ const executeWorkflowAction = async (action: string) => {
+ setActionProgress(0)
+ setIsLoading(true)
+
+ try {
+ // 진행률 시뮬레이션
+ const progressInterval = setInterval(() => {
+ setActionProgress(prev => Math.min(prev + 20, 90))
+ }, 200)
+
+ const response = await fetch('/api/sync/workflow/action', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ contractId,
+ targetSystem: 'SWP',
+ action,
+ documents: documents.map(doc => ({ id: doc.id, documentNo: doc.documentNo }))
+ })
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json()
+ throw new Error(errorData.message || 'Workflow action failed')
+ }
+
+ const result = await response.json()
+
+ clearInterval(progressInterval)
+ setActionProgress(100)
+
+ setTimeout(() => {
+ setActionProgress(0)
+
+ if (result?.success) {
+ toast.success(
+ `${getActionLabel(action)} 완료`,
+ { description: result?.message || '워크플로우가 성공적으로 진행되었습니다.' }
+ )
+ } else {
+ toast.error(
+ `${getActionLabel(action)} 실패`,
+ { description: result?.message || '워크플로우 실행에 실패했습니다.' }
+ )
+ }
+
+ fetchWorkflowStatus() // 상태 갱신
+ onWorkflowUpdate?.()
+ }, 500)
+
+ } catch (error) {
+ setActionProgress(0)
+
+ toast.error(`${getActionLabel(action)} 실패`, {
+ description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.'
+ })
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const getActionLabel = (action: string): string => {
+ switch (action) {
+ case 'SUBMIT_LIST': return '목록 전송'
+ case 'CHECK_CONFIRMATION': return '컨펌 확인'
+ case 'RESUBMIT_REVISED': return '수정본 재전송'
+ case 'CHECK_APPROVAL': return '승인 확인'
+ default: return action
+ }
+ }
+
+ const getStatusBadge = () => {
+ if (isLoading) {
+ return <Badge variant="secondary">확인 중...</Badge>
+ }
+
+ if (!workflowState) {
+ return <Badge variant="destructive">오류</Badge>
+ }
+
+ switch (workflowState.status) {
+ case 'IDLE':
+ return <Badge variant="secondary">대기</Badge>
+ case 'SUBMITTED':
+ return (
+ <Badge variant="default" className="gap-1 bg-blue-500">
+ <Clock className="w-3 h-3" />
+ 전송됨
+ </Badge>
+ )
+ case 'UNDER_REVIEW':
+ return (
+ <Badge variant="default" className="gap-1 bg-yellow-500">
+ <Eye className="w-3 h-3" />
+ 검토 중
+ </Badge>
+ )
+ case 'CONFIRMED':
+ return (
+ <Badge variant="default" className="gap-1 bg-green-500">
+ <CheckCircle className="w-3 h-3" />
+ 컨펌됨
+ </Badge>
+ )
+ case 'REVISION_REQUIRED':
+ return (
+ <Badge variant="destructive" className="gap-1">
+ <AlertTriangle className="w-3 h-3" />
+ 수정 요청
+ </Badge>
+ )
+ case 'RESUBMITTED':
+ return (
+ <Badge variant="default" className="gap-1 bg-orange-500">
+ <RefreshCw className="w-3 h-3" />
+ 재전송됨
+ </Badge>
+ )
+ case 'APPROVED':
+ return (
+ <Badge variant="default" className="gap-1 bg-green-600">
+ <CheckCircle className="w-3 h-3" />
+ 승인 완료
+ </Badge>
+ )
+ default:
+ return <Badge variant="secondary">알 수 없음</Badge>
+ }
+ }
+
+ const getAvailableActions = (): string[] => {
+ if (!workflowState) return []
+
+ switch (workflowState.status) {
+ case 'IDLE':
+ return ['SUBMIT_LIST']
+ case 'SUBMITTED':
+ return ['CHECK_CONFIRMATION']
+ case 'UNDER_REVIEW':
+ return ['CHECK_CONFIRMATION']
+ case 'CONFIRMED':
+ return [] // 컨펌되면 자동으로 다음 단계로
+ case 'REVISION_REQUIRED':
+ return ['RESUBMIT_REVISED']
+ case 'RESUBMITTED':
+ return ['CHECK_APPROVAL']
+ case 'APPROVED':
+ return [] // 완료 상태
+ default:
+ return []
+ }
+ }
+
+ const availableActions = getAvailableActions()
+
+ return (
+ <Popover>
+ <PopoverTrigger asChild>
+ <div className="flex items-center gap-3">
+ <Button
+ variant="outline"
+ size="sm"
+ className="flex items-center border-orange-200 hover:bg-orange-50"
+ disabled={isLoading}
+ >
+ {isLoading ? (
+ <Loader2 className="w-4 h-4 animate-spin" />
+ ) : (
+ <RefreshCw className="w-4 h-4" />
+ )}
+ <span className="hidden sm:inline">SWP 워크플로우</span>
+ {workflowState?.pendingActions && workflowState.pendingActions.length > 0 && (
+ <Badge
+ variant="destructive"
+ className="h-5 w-5 p-0 text-xs flex items-center justify-center"
+ >
+ {workflowState.pendingActions.length}
+ </Badge>
+ )}
+ </Button>
+ </div>
+ </PopoverTrigger>
+
+ <PopoverContent className="w-80">
+ <div className="space-y-4">
+ <div className="space-y-2">
+ <h4 className="font-medium">SWP 워크플로우 상태</h4>
+ <div className="flex items-center justify-between">
+ <span className="text-sm text-muted-foreground">현재 상태</span>
+ {getStatusBadge()}
+ </div>
+ </div>
+
+ {workflowState && (
+ <div className="space-y-3">
+ <Separator />
+
+ {/* 대기 중인 액션들 */}
+ {workflowState.pendingActions && workflowState.pendingActions.length > 0 && (
+ <div className="space-y-2">
+ <div className="text-sm font-medium">대기 중인 작업</div>
+ {workflowState.pendingActions.map((action, index) => (
+ <Badge key={index} variant="outline" className="mr-1">
+ {getActionLabel(action)}
+ </Badge>
+ ))}
+ </div>
+ )}
+
+ {/* 수정 요청 사항 */}
+ {workflowState.revisionComments && workflowState.revisionComments.length > 0 && (
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-red-600">수정 요청 사항</div>
+ <div className="text-xs text-muted-foreground space-y-1">
+ {workflowState.revisionComments.map((comment, index) => (
+ <div key={index} className="p-2 bg-red-50 rounded text-red-700">
+ {comment}
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {/* 마지막 업데이트 시간 */}
+ {workflowState.lastUpdatedAt && (
+ <div className="text-sm">
+ <div className="text-muted-foreground">마지막 업데이트</div>
+ <div className="font-medium">
+ {new Date(workflowState.lastUpdatedAt).toLocaleString()}
+ </div>
+ </div>
+ )}
+
+ {/* 진행률 표시 */}
+ {isLoading && actionProgress > 0 && (
+ <div className="space-y-2">
+ <div className="flex items-center justify-between text-sm">
+ <span>진행률</span>
+ <span>{actionProgress}%</span>
+ </div>
+ <Progress value={actionProgress} className="h-2" />
+ </div>
+ )}
+ </div>
+ )}
+
+ <Separator />
+
+ {/* 액션 버튼들 */}
+ <div className="space-y-2">
+ {availableActions.length > 0 ? (
+ availableActions.map((action) => (
+ <Button
+ key={action}
+ onClick={() => executeWorkflowAction(action)}
+ disabled={isLoading}
+ className="w-full justify-start"
+ size="sm"
+ variant={action.includes('SUBMIT') || action.includes('RESUBMIT') ? 'default' : 'outline'}
+ >
+ {isLoading ? (
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+ ) : (
+ <Send className="w-4 h-4 mr-2" />
+ )}
+ {getActionLabel(action)}
+ </Button>
+ ))
+ ) : (
+ <div className="text-sm text-muted-foreground text-center py-2">
+ {workflowState?.status === 'APPROVED'
+ ? '워크플로우가 완료되었습니다.'
+ : '실행 가능한 작업이 없습니다.'}
+ </div>
+ )}
+
+ {/* 상태 새로고침 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={fetchWorkflowStatus}
+ disabled={isLoading}
+ className="w-full"
+ >
+ {isLoading ? (
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+ ) : (
+ <RefreshCw className="w-4 h-4 mr-2" />
+ )}
+ 상태 새로고침
+ </Button>
+ </div>
+ </div>
+ </PopoverContent>
+ </Popover>
+ )
+} \ No newline at end of file