summaryrefslogtreecommitdiff
path: root/lib/rfq-last/attachment/rfq-attachments-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfq-last/attachment/rfq-attachments-table.tsx')
-rw-r--r--lib/rfq-last/attachment/rfq-attachments-table.tsx370
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