"use client"; import * as React from "react"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Download, FileText, Upload, RefreshCw, Eye, Trash2, History, Plus, File, FileImage, FileSpreadsheet, FileCode } from "lucide-react"; import { format, formatDistanceToNow } from "date-fns"; import { ko } from "date-fns/locale"; import { type ColumnDef } from "@tanstack/react-table"; import { Checkbox } from "@/components/ui/checkbox"; import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header"; import { ClientDataTable } from "@/components/client-data-table/data-table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import type { DataTableAdvancedFilterField, DataTableRowAction, } from "@/types/table"; import { cn } from "@/lib/utils"; import { getRfqAllAttachments } from "@/lib/rfq-last/service"; import { downloadFile } from "@/lib/file-download"; import { DeleteAttachmentsDialog } from "./delete-attachments-dialog"; import { AddAttachmentDialog } from "./add-attachment-dialog"; import { UpdateRevisionDialog } from "./update-revision-dialog"; import { toast } from "sonner"; import { RevisionHistoryDialog } from "./revision-historty-dialog"; import { createFilterFn } from "@/components/client-data-table/table-filters"; // 타입 정의 interface RfqAttachment { id: number; attachmentType: "설계" | "구매"; serialNo: string | null; rfqId: number; currentRevision: string | null; latestRevisionId: number | null; description: string | null; createdBy: number; createdAt: Date; updatedAt: Date; fileName: string | null; originalFileName: string | null; filePath: string | null; fileSize: number | null; fileType: string | null; revisionComment: string | null; createdByName: string | null; } interface RfqAttachmentsTableProps { rfqId: number; initialData: RfqAttachment[]; } // 파일 타입별 아이콘 반환 const getFileIcon = (fileType: string | null) => { if (!fileType) return ; const type = fileType.toLowerCase(); if (type.includes('image') || ['jpg', 'jpeg', 'png', 'gif'].includes(type)) { return ; } if (type.includes('excel') || type.includes('spreadsheet') || ['xls', 'xlsx'].includes(type)) { return ; } if (type.includes('pdf')) { return ; } if (type.includes('code') || ['js', 'ts', 'tsx', 'jsx', 'html', 'css'].includes(type)) { return ; } return ; }; // 파일 크기 포맷팅 const formatFileSize = (bytes: number | null) => { if (!bytes) return "-"; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; }; export function RfqAttachmentsTable({ rfqId, initialData, }: RfqAttachmentsTableProps) { const [activeTab, setActiveTab] = React.useState<'설계' | '구매'>('설계'); const [data, setData] = React.useState(initialData); const [selectedAttachment, setSelectedAttachment] = React.useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); const [updateRevisionDialogOpen, setUpdateRevisionDialogOpen] = React.useState(false); const [revisionHistoryDialogOpen, setRevisionHistoryDialogOpen] = React.useState(false); const [addDialogOpen, setAddDialogOpen] = React.useState(false); const [isRefreshing, setIsRefreshing] = React.useState(false); const [selectedRows, setSelectedRows] = React.useState([]); // 탭에 따른 데이터 필터링 const filteredData = React.useMemo(() => { return data.filter(item => item.attachmentType === activeTab); }, [data, activeTab]); // 데이터 새로고침 const handleRefresh = React.useCallback(async () => { setIsRefreshing(true); try { const result = await getRfqAllAttachments(rfqId); if (result.success && result.data) { setData(result.data); toast.success("데이터를 새로고침했습니다."); } else { toast.error("데이터를 불러오는데 실패했습니다."); } } catch (error) { console.error("Refresh error:", error); toast.error("새로고침 중 오류가 발생했습니다."); } finally { setIsRefreshing(false); } }, [rfqId]); // 액션 처리 const handleAction = React.useCallback(async (action: DataTableRowAction) => { const attachment = action.row.original; switch (action.type) { case "download": if (attachment.filePath && attachment.originalFileName) { await downloadFile(attachment.filePath, attachment.originalFileName, { action: 'download', showToast: true }); } break; case "preview": if (attachment.filePath && attachment.originalFileName) { await downloadFile(attachment.filePath, attachment.originalFileName, { action: 'preview', showToast: true }); } break; case "history": setSelectedAttachment(attachment); setRevisionHistoryDialogOpen(true); break; case "update": setSelectedAttachment(attachment); setUpdateRevisionDialogOpen(true); break; case "delete": setSelectedAttachment(attachment); setDeleteDialogOpen(true); break; } }, []); // 선택된 항목 일괄 삭제 const handleBulkDelete = React.useCallback(() => { if (selectedRows.length === 0) { toast.warning("삭제할 항목을 선택해주세요."); return; } setDeleteDialogOpen(true); }, [selectedRows]); // 선택된 항목 일괄 다운로드 const handleBulkDownload = React.useCallback(async () => { if (selectedRows.length === 0) { toast.warning("다운로드할 항목을 선택해주세요."); return; } for (const attachment of selectedRows) { if (attachment.filePath && attachment.originalFileName) { await downloadFile(attachment.filePath, attachment.originalFileName, { action: 'download', showToast: false }); } } toast.success(`${selectedRows.length}개 파일을 다운로드했습니다.`); }, [selectedRows]); // 컬럼 정의 const columns: ColumnDef[] = React.useMemo(() => [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!v)} aria-label="select all" className="translate-y-0.5" /> ), cell: ({ row }) => ( row.toggleSelected(!!v)} aria-label="select row" className="translate-y-0.5" /> ), size: 40, enableSorting: false, enableHiding: false, enablePinning: true, }, { accessorKey: "serialNo", header: ({ column }) => , filterFn: createFilterFn("text"), // 추가 cell: ({ row }) => ( {row.original.serialNo || "-"} ), size: 100, meta: { excelHeader: "일련번호" }, enablePinning: true, }, { accessorKey: "originalFileName", header: ({ column }) => , filterFn: createFilterFn("text"), // 추가 cell: ({ row }) => { const file = row.original; return (
{getFileIcon(file.fileType)}
{file.originalFileName || file.fileName || "-"}
); }, size: 300, }, { accessorKey: "description", header: ({ column }) => , filterFn: createFilterFn("text"), // 추가 cell: ({ row }) => (
{row.original.description || "-"}
), size: 200, }, { accessorKey: "currentRevision", header: ({ column }) => , filterFn: createFilterFn("text"), // 추가 cell: ({ row }) => { const revision = row.original.currentRevision; return revision ? ( Rev. {revision} ) : ( - ); }, size: 100, }, { accessorKey: "fileSize", header: ({ column }) => , filterFn: createFilterFn("number"), // number 타입으로 변경 cell: ({ row }) => ( {formatFileSize(row.original.fileSize)} ), size: 80, }, { accessorKey: "fileType", header: ({ column }) => , filterFn: createFilterFn("select"), // 추가 cell: ({ row }) => { const fileType = row.original.fileType; if (!fileType) return -; const type = fileType.toLowerCase(); let displayType = "기타"; let color = "text-gray-500"; if (type.includes('pdf')) { displayType = "PDF"; color = "text-red-500"; } else if (type.includes('excel') || ['xls', 'xlsx'].includes(type)) { displayType = "Excel"; color = "text-green-500"; } else if (type.includes('word') || ['doc', 'docx'].includes(type)) { displayType = "Word"; color = "text-blue-500"; } else if (type.includes('image') || ['jpg', 'jpeg', 'png', 'gif'].includes(type)) { displayType = "이미지"; color = "text-purple-500"; } return ( {displayType} ); }, size: 100, }, { accessorKey: "createdByName", header: ({ column }) => , filterFn: createFilterFn("text"), // 추가 cell: ({ row }) => row.original.createdByName || "-", size: 100, }, { accessorKey: "createdAt", header: ({ column }) => , filterFn: createFilterFn("date"), // date 타입으로 변경 cell: ({ row }) => { const date = row.original.createdAt; return date ? ( {format(new Date(date), "MM-dd HH:mm")}

{format(new Date(date), "yyyy년 MM월 dd일 HH시 mm분")}

({formatDistanceToNow(new Date(date), { addSuffix: true, locale: ko })})

) : ( "-" ); }, size: 100, }, { accessorKey: "updatedAt", header: ({ column }) => , filterFn: createFilterFn("date"), // date 타입으로 변경 cell: ({ row }) => { const date = row.original.updatedAt; return date ? format(new Date(date), "MM-dd HH:mm") : "-"; }, size: 100, }, { accessorKey: "revisionComment", header: ({ column }) => , filterFn: createFilterFn("text"), // 추가 cell: ({ row }) => { const comment = row.original.revisionComment; return comment ? ( {comment}

{comment}

) : ( - ); }, size: 150, }, { id: "actions", header: "작업", cell: ({ row }) => { // 구매 탭에서만 작업 버튼 표시 if (activeTab !== '구매') { return -; } return ( handleAction({ type: "download", row })}> 다운로드 {/* handleAction({ type: "preview", row })}> 미리보기 */} handleAction({ type: "history", row })}> 리비전 히스토리 handleAction({ type: "update", row })}> 새 버전 업로드 handleAction({ type: "delete", row })} className="text-red-600" > 삭제 ); }, size: 60, enablePinning: true, }, ], [handleAction, activeTab]); const advancedFilterFields: DataTableAdvancedFilterField[] = [ { id: "serialNo", label: "일련번호", type: "text" }, { id: "originalFileName", label: "파일명", type: "text" }, { id: "description", label: "설명", type: "text" }, { id: "currentRevision", label: "리비전", type: "text" }, // { // id: "fileType", // label: "파일 타입", // type: "select", // options: [ // { label: "PDF", value: "pdf" }, // { label: "Excel", value: "xlsx" }, // { label: "Word", value: "docx" }, // { label: "이미지", value: "image" }, // { label: "기타", value: "other" }, // ] // }, { id: "createdByName", label: "업로드자", type: "text" }, { id: "createdAt", label: "업로드일", type: "date" }, { id: "updatedAt", label: "수정일", type: "date" }, ]; // 탭별 데이터 카운트 const designCount = React.useMemo(() => data.filter(item => item.attachmentType === "설계").length, [data] ); const purchaseCount = React.useMemo(() => data.filter(item => item.attachmentType === "구매").length, [data] ); // 추가 액션 버튼들 const additionalActions = React.useMemo(() => (
{selectedRows.length > 0 && ( <> )} {/* 구매 탭에서만 파일 업로드 버튼 표시 */} {activeTab === "구매" && ( )}
), [selectedRows, activeTab, isRefreshing, addDialogOpen, handleBulkDownload, handleBulkDelete, handleRefresh, rfqId]); return (
setActiveTab(value as '설계' | '구매')} >
설계 첨부파일 {designCount} 구매 첨부파일 {purchaseCount}
{additionalActions} {additionalActions}
{/* 삭제 다이얼로그 */} {(selectedAttachment || selectedRows.length > 0) && ( { setDeleteDialogOpen(open); if (!open) { setSelectedAttachment(null); } }} attachments={selectedAttachment ? [selectedAttachment] : selectedRows} onSuccess={handleRefresh} /> )} {/* 새 버전 업로드 다이얼로그 */} {selectedAttachment && ( { setUpdateRevisionDialogOpen(open); if (!open) { setSelectedAttachment(null); } }} attachment={selectedAttachment} onSuccess={handleRefresh} /> )} {/* 리비전 히스토리 다이얼로그 */} {selectedAttachment && ( { setRevisionHistoryDialogOpen(open); if (!open) { setSelectedAttachment(null); } }} attachmentId={selectedAttachment.id} attachmentName={selectedAttachment.originalFileName || selectedAttachment.fileName || undefined} /> )}
); }