diff options
Diffstat (limited to 'lib/vendor-document-list/table')
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 |
