diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-05 11:44:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-05 11:44:32 +0000 |
| commit | 50adedf48ee4674ebe00f1ee72d93485183cdc51 (patch) | |
| tree | 18053ab04d94c750028eee5d5d2f16ba4f38f50e /lib/rfq-last/attachment/rfq-attachments-table.tsx | |
| parent | 66d64b482f2b6b52b0dd396ef998f27d491c70dd (diff) | |
(대표님, 최겸, 임수민) EDP 입력 진행률, 견적목록관리, EDP excel import 오류수정, GTC-Contract
Diffstat (limited to 'lib/rfq-last/attachment/rfq-attachments-table.tsx')
| -rw-r--r-- | lib/rfq-last/attachment/rfq-attachments-table.tsx | 370 |
1 files changed, 205 insertions, 165 deletions
diff --git a/lib/rfq-last/attachment/rfq-attachments-table.tsx b/lib/rfq-last/attachment/rfq-attachments-table.tsx index a66e12a2..155fd412 100644 --- a/lib/rfq-last/attachment/rfq-attachments-table.tsx +++ b/lib/rfq-last/attachment/rfq-attachments-table.tsx @@ -1,7 +1,6 @@ "use client"; import * as React from "react"; -import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent } from "@/components/ui/card"; @@ -24,10 +23,8 @@ 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 { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; -import { useDataTable } from "@/hooks/use-data-table"; -import { DataTable } from "@/components/data-table/data-table"; -import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; +import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header"; +import { ClientDataTable } from "@/components/client-data-table/data-table"; import { DropdownMenu, DropdownMenuContent, @@ -43,16 +40,16 @@ import { } from "@/components/ui/tooltip"; import type { DataTableAdvancedFilterField, - DataTableFilterField, DataTableRowAction, } from "@/types/table"; import { cn } from "@/lib/utils"; -import { getRfqLastAttachments } from "@/lib/rfq-last/service"; +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 { useQueryState ,parseAsStringEnum} from "nuqs"; +import { toast } from "sonner"; +import { RevisionHistoryDialog } from "./revision-historty-dialog"; // 타입 정의 interface RfqAttachment { @@ -77,9 +74,7 @@ interface RfqAttachment { interface RfqAttachmentsTableProps { rfqId: number; - initialDesignData: Awaited<ReturnType<typeof getRfqLastAttachments>>; - initialPurchaseData: Awaited<ReturnType<typeof getRfqLastAttachments>>; - className?: string; + initialData: RfqAttachment[]; } // 파일 타입별 아이콘 반환 @@ -112,31 +107,41 @@ const formatFileSize = (bytes: number | null) => { export function RfqAttachmentsTable({ rfqId, - initialDesignData, - initialPurchaseData, - className + initialData, }: RfqAttachmentsTableProps) { - const router = useRouter(); - const [activeTab, setActiveTab] = useQueryState( - 'tab', - parseAsStringEnum(['설계', '구매']) - .withDefault('설계') - .withOptions({ shallow: false }) - ); - - const [designData] = React.useState(initialDesignData); - const [purchaseData] = React.useState(initialPurchaseData); + const [activeTab, setActiveTab] = React.useState<'설계' | '구매'>('설계'); + const [data, setData] = React.useState<RfqAttachment[]>(initialData); const [selectedAttachment, setSelectedAttachment] = React.useState<RfqAttachment | null>(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<RfqAttachment[]>([]); + + // 탭에 따른 데이터 필터링 + const filteredData = React.useMemo(() => { + return data.filter(item => item.attachmentType === activeTab); + }, [data, activeTab]); - // 새로고침 (router.refresh 사용) - const handleRefresh = React.useCallback(() => { + // 데이터 새로고침 + const handleRefresh = React.useCallback(async () => { setIsRefreshing(true); - router.refresh(); - setTimeout(() => setIsRefreshing(false), 1000); - }, [router]); + 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<RfqAttachment>) => { @@ -162,8 +167,8 @@ export function RfqAttachmentsTable({ break; case "history": - // 리비전 이력 보기 - 별도 구현 필요 - console.log("History:", attachment); + setSelectedAttachment(attachment); + setRevisionHistoryDialogOpen(true); break; case "update": @@ -178,10 +183,35 @@ export function RfqAttachmentsTable({ } }, []); + // 선택된 항목 일괄 삭제 + 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 getAttachmentColumns = React.useCallback(( - onAction: (action: DataTableRowAction<RfqAttachment>) => void - ): ColumnDef<RfqAttachment>[] => [ + const columns: ColumnDef<RfqAttachment>[] = React.useMemo(() => [ { id: "select", header: ({ table }) => ( @@ -203,18 +233,21 @@ export function RfqAttachmentsTable({ size: 40, enableSorting: false, enableHiding: false, + enablePinning: true, }, { accessorKey: "serialNo", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="일련번호" />, + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="일련번호" />, cell: ({ row }) => ( <span className="font-mono text-sm">{row.original.serialNo || "-"}</span> ), size: 100, + meta: { excelHeader: "일련번호" }, + enablePinning: true, }, { accessorKey: "originalFileName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="파일명" />, + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="파일명" />, cell: ({ row }) => { const file = row.original; return ( @@ -224,11 +257,6 @@ export function RfqAttachmentsTable({ <span className="text-sm font-medium truncate max-w-[250px]" title={file.originalFileName || ""}> {file.originalFileName || file.fileName || "-"} </span> - {file.fileName && file.fileName !== file.originalFileName && ( - <span className="text-xs text-muted-foreground truncate max-w-[250px]"> - ({file.fileName}) - </span> - )} </div> </div> ); @@ -237,7 +265,7 @@ export function RfqAttachmentsTable({ }, { accessorKey: "description", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="설명" />, + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="설명" />, cell: ({ row }) => ( <div className="max-w-[200px] truncate" title={row.original.description || ""}> {row.original.description || "-"} @@ -247,7 +275,7 @@ export function RfqAttachmentsTable({ }, { accessorKey: "currentRevision", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="리비전" />, + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="리비전" />, cell: ({ row }) => { const revision = row.original.currentRevision; return revision ? ( @@ -262,7 +290,7 @@ export function RfqAttachmentsTable({ }, { accessorKey: "fileSize", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="크기" />, + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="크기" />, cell: ({ row }) => ( <span className="text-sm text-muted-foreground"> {formatFileSize(row.original.fileSize)} @@ -271,29 +299,14 @@ export function RfqAttachmentsTable({ size: 80, }, { - accessorKey: "fileType", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="타입" />, - cell: ({ row }) => { - const type = row.original.fileType; - return type ? ( - <Badge variant="secondary" className="text-xs"> - {type.toUpperCase()} - </Badge> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, - size: 80, - }, - { accessorKey: "createdByName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="업로드자" />, + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="업로드자" />, cell: ({ row }) => row.original.createdByName || "-", size: 100, }, { accessorKey: "createdAt", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="업로드일" />, + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="업로드일" />, cell: ({ row }) => { const date = row.original.createdAt; return date ? ( @@ -320,7 +333,7 @@ export function RfqAttachmentsTable({ }, { accessorKey: "updatedAt", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="수정일" />, + header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="수정일" />, cell: ({ row }) => { const date = row.original.updatedAt; return date ? format(new Date(date), "MM-dd HH:mm") : "-"; @@ -342,26 +355,26 @@ export function RfqAttachmentsTable({ </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> - <DropdownMenuItem onClick={() => onAction({ row, type: "download" })}> + <DropdownMenuItem onClick={() => handleAction({ row, type: "download" })}> <Download className="mr-2 h-4 w-4" /> 다운로드 </DropdownMenuItem> - <DropdownMenuItem onClick={() => onAction({ row, type: "preview" })}> + <DropdownMenuItem onClick={() => handleAction({ row, type: "preview" })}> <Eye className="mr-2 h-4 w-4" /> 미리보기 </DropdownMenuItem> <DropdownMenuSeparator /> - <DropdownMenuItem onClick={() => onAction({ row, type: "history" })}> + <DropdownMenuItem onClick={() => handleAction({ row, type: "history" })}> <History className="mr-2 h-4 w-4" /> 리비전 이력 </DropdownMenuItem> - <DropdownMenuItem onClick={() => onAction({ row, type: "update" })}> + <DropdownMenuItem onClick={() => handleAction({ row, type: "update" })}> <Upload className="mr-2 h-4 w-4" /> 새 버전 업로드 </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem - onClick={() => onAction({ row, type: "delete" })} + onClick={() => handleAction({ row, type: "delete" })} className="text-red-600" > <Trash2 className="mr-2 h-4 w-4" /> @@ -372,17 +385,9 @@ export function RfqAttachmentsTable({ ); }, size: 60, + enablePinning: true, }, - ], []); - - const columns = React.useMemo(() => getAttachmentColumns(handleAction), [getAttachmentColumns, handleAction]); - - const filterFields: DataTableFilterField<RfqAttachment>[] = [ - { id: "serialNo", label: "일련번호" }, - { id: "originalFileName", label: "파일명" }, - { id: "description", label: "설명" }, - { id: "createdByName", label: "업로드자" }, - ]; + ], [handleAction]); const advancedFilterFields: DataTableAdvancedFilterField<RfqAttachment>[] = [ { id: "serialNo", label: "일련번호", type: "text" }, @@ -406,121 +411,136 @@ export function RfqAttachmentsTable({ { id: "updatedAt", label: "수정일", type: "date" }, ]; - const { table: designTable } = useDataTable({ - data: designData.data, - columns, - pageCount: designData.pageCount, - rowCount: designData.data.length, - filterFields, - enableAdvancedFilter: true, - // 설계 탭용 파라미터 prefix - paramPrefix: 'design_', - initialState: { - sorting: [{ id: "createdAt", desc: true }], - }, - getRowId: (row) => String(row.id), - shallow: false, - clearOnDefault: true, - }); - - const { table: purchaseTable } = useDataTable({ - data: purchaseData.data, - columns, - pageCount: purchaseData.pageCount, - rowCount: purchaseData.data.length, - filterFields, - enableAdvancedFilter: true, - // 구매 탭용 파라미터 prefix - paramPrefix: 'purchase_', - initialState: { - sorting: [{ id: "createdAt", desc: true }], - }, - getRowId: (row) => String(row.id), - shallow: false, - clearOnDefault: true, - }); - + // 탭별 데이터 카운트 + const designCount = React.useMemo(() => + data.filter(item => item.attachmentType === "설계").length, [data] + ); + const purchaseCount = React.useMemo(() => + data.filter(item => item.attachmentType === "구매").length, [data] + ); - React.useEffect(() => { - router.refresh(); - }, [activeTab]); + // 추가 액션 버튼들 + const additionalActions = React.useMemo(() => ( + <div className="flex items-center gap-2"> + {selectedRows.length > 0 && ( + <> + <Button + variant="outline" + size="sm" + onClick={handleBulkDownload} + > + <Download className="h-4 w-4 mr-2" /> + 다운로드 ({selectedRows.length}) + </Button> + <Button + variant="outline" + size="sm" + onClick={handleBulkDelete} + className="text-red-600" + > + <Trash2 className="h-4 w-4 mr-2" /> + 삭제 ({selectedRows.length}) + </Button> + </> + )} + <Button + variant="outline" + size="sm" + onClick={handleRefresh} + disabled={isRefreshing} + > + <RefreshCw className={cn("h-4 w-4 mr-2", isRefreshing && "animate-spin")} /> + 새로고침 + </Button> + + {/* 구매 탭에서만 파일 업로드 버튼 표시 */} + {activeTab === "구매" && ( + <AddAttachmentDialog + rfqId={rfqId} + attachmentType="구매" + onSuccess={handleRefresh} + open={addDialogOpen} + onOpenChange={setAddDialogOpen} + /> + )} + </div> + ), [selectedRows, activeTab, isRefreshing, addDialogOpen, handleBulkDownload, handleBulkDelete, handleRefresh, rfqId]); return ( - <div className={cn("w-full space-y-4", className)}> - <Tabs value={activeTab} onValueChange={setActiveTab}> + <div className={cn("w-full space-y-4")}> + <Tabs + value={activeTab} + onValueChange={(value) => setActiveTab(value as '설계' | '구매')} + > <div className="flex items-center justify-between mb-4"> <TabsList> <TabsTrigger value="설계"> 설계 첨부파일 <Badge variant="secondary" className="ml-2"> - {designData.data.length} + {designCount} </Badge> </TabsTrigger> <TabsTrigger value="구매"> 구매 첨부파일 <Badge variant="secondary" className="ml-2"> - {purchaseData.data.length} + {purchaseCount} </Badge> </TabsTrigger> </TabsList> - - <div className="flex items-center gap-2"> - <Button - variant="outline" - size="sm" - onClick={handleRefresh} - disabled={isRefreshing} - > - <RefreshCw className={cn("h-4 w-4 mr-2", isRefreshing && "animate-spin")} /> - 새로고침 - </Button> - - {/* 구매 탭에서만 파일 업로드 버튼 표시 */} - {activeTab === "구매" && ( - <AddAttachmentDialog - rfqId={rfqId} - attachmentType="구매" - onSuccess={handleRefresh} - /> - )} - </div> </div> <TabsContent value="설계" className="mt-0"> - <Card> - <CardContent className="p-0"> - <DataTable table={designTable}> - <DataTableAdvancedToolbar - table={designTable} - filterFields={advancedFilterFields} - shallow={false} - /> - </DataTable> - </CardContent> - </Card> + + <ClientDataTable + columns={columns} + data={filteredData} + advancedFilterFields={advancedFilterFields} + autoSizeColumns={true} + compact={true} + maxHeight="34rem" + onSelectedRowsChange={setSelectedRows} + initialColumnPinning={{ + left: ["select", "serialNo"], + right: ["actions"], + }} + > + {additionalActions} + </ClientDataTable> + </TabsContent> <TabsContent value="구매" className="mt-0"> - <Card> - <CardContent className="p-0"> - <DataTable table={purchaseTable}> - <DataTableAdvancedToolbar - table={purchaseTable} - filterFields={advancedFilterFields} - shallow={false} - /> - </DataTable> - </CardContent> - </Card> + + <ClientDataTable + columns={columns} + data={filteredData} + advancedFilterFields={advancedFilterFields} + autoSizeColumns={true} + compact={true} + maxHeight="34rem" + onSelectedRowsChange={setSelectedRows} + initialColumnPinning={{ + left: ["select", "serialNo"], + right: ["actions"], + }} + > + {additionalActions} + </ClientDataTable> + </TabsContent> </Tabs> {/* 삭제 다이얼로그 */} - {selectedAttachment && ( + {(selectedAttachment || selectedRows.length > 0) && ( <DeleteAttachmentsDialog open={deleteDialogOpen} - onOpenChange={setDeleteDialogOpen} - attachments={[selectedAttachment]} + onOpenChange={(open) => { + setDeleteDialogOpen(open); + if (!open) { + setSelectedAttachment(null); + } + }} + attachments={selectedAttachment ? [selectedAttachment] : selectedRows} onSuccess={handleRefresh} /> )} @@ -529,11 +549,31 @@ export function RfqAttachmentsTable({ {selectedAttachment && ( <UpdateRevisionDialog open={updateRevisionDialogOpen} - onOpenChange={setUpdateRevisionDialogOpen} + onOpenChange={(open) => { + setUpdateRevisionDialogOpen(open); + if (!open) { + setSelectedAttachment(null); + } + }} attachment={selectedAttachment} onSuccess={handleRefresh} /> )} + + {/* 리비전 히스토리 다이얼로그 */} + {selectedAttachment && ( + <RevisionHistoryDialog + open={revisionHistoryDialogOpen} + onOpenChange={(open) => { + setRevisionHistoryDialogOpen(open); + if (!open) { + setSelectedAttachment(null); + } + }} + attachmentId={selectedAttachment.id} + attachmentName={selectedAttachment.originalFileName || selectedAttachment.fileName || undefined} + /> + )} </div> ); }
\ No newline at end of file |
