diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-20 11:37:31 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-20 11:37:31 +0000 |
| commit | aa86729f9a2ab95346a2851e3837de1c367aae17 (patch) | |
| tree | b601b18b6724f2fb449c7fa9ea50cbd652a8077d /lib/project-gtc/table/project-gtc-table-columns.tsx | |
| parent | 95bbe9c583ff841220da1267630e7b2025fc36dc (diff) | |
(대표님) 20250620 작업사항
Diffstat (limited to 'lib/project-gtc/table/project-gtc-table-columns.tsx')
| -rw-r--r-- | lib/project-gtc/table/project-gtc-table-columns.tsx | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/lib/project-gtc/table/project-gtc-table-columns.tsx b/lib/project-gtc/table/project-gtc-table-columns.tsx new file mode 100644 index 00000000..dfdf1921 --- /dev/null +++ b/lib/project-gtc/table/project-gtc-table-columns.tsx @@ -0,0 +1,364 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis, Paperclip, FileText } from "lucide-react" +import { toast } from "sonner" + +import { formatDate, formatDateTime } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { ProjectGtcView } from "@/db/schema" + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ProjectGtcView> | null>> +} + +/** + * 파일 다운로드 함수 + */ +const handleFileDownload = async (projectId: number, fileName: string) => { + try { + // API를 통해 파일 다운로드 + const response = await fetch(`/api/project-gtc?action=download&projectId=${projectId}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('파일 다운로드에 실패했습니다.'); + } + + // 파일 blob 생성 + const blob = await response.blob(); + + // 다운로드 링크 생성 + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // 메모리 정리 + window.URL.revokeObjectURL(url); + + toast.success("파일 다운로드를 시작합니다."); + } catch (error) { + console.error("파일 다운로드 오류:", error); + toast.error("파일 다운로드 중 오류가 발생했습니다."); + } +}; + +/** + * tanstack table 컬럼 정의 (중첩 헤더 버전) + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ProjectGtcView>[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef<ProjectGtcView> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + maxSize: 30, + enableSorting: false, + enableHiding: false, + } + + // ---------------------------------------------------------------- + // 2) 파일 다운로드 컬럼 (아이콘) + // ---------------------------------------------------------------- + const downloadColumn: ColumnDef<ProjectGtcView> = { + id: "download", + header: "", + cell: ({ row }) => { + const project = row.original; + + if (!project.filePath || !project.originalFileName) { + return null; + } + + return ( + <Button + variant="ghost" + size="icon" + onClick={() => handleFileDownload(project.id, project.originalFileName!)} + title={`${project.originalFileName} 다운로드`} + className="hover:bg-muted" + > + <Paperclip className="h-4 w-4" /> + <span className="sr-only">다운로드</span> + </Button> + ); + }, + maxSize: 30, + enableSorting: false, + } + + // ---------------------------------------------------------------- + // 3) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef<ProjectGtcView> = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="Open menu" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-40"> + <DropdownMenuItem + onSelect={() => setRowAction({ type: "upload", row })} + > + Edit + </DropdownMenuItem> + + <DropdownMenuSeparator /> + <DropdownMenuItem + onSelect={() => setRowAction({ type: "delete", row })} + > + Delete + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) + }, + maxSize: 30, + } + + // ---------------------------------------------------------------- + // 4) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 + // ---------------------------------------------------------------- + // 4-1) groupMap: { [groupName]: ColumnDef<ProjectGtcView>[] } + const groupMap: Record<string, ColumnDef<ProjectGtcView>[]> = {} + + // 프로젝트 정보 그룹 + groupMap["기본 정보"] = [ + { + accessorKey: "code", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> + ), + cell: ({ row }) => { + return ( + <div className="flex space-x-2"> + <span className="max-w-[500px] truncate font-medium"> + {row.getValue("code")} + </span> + </div> + ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + minSize: 120, + }, + { + accessorKey: "name", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트명" /> + ), + cell: ({ row }) => { + return ( + <div className="flex space-x-2"> + <span className="max-w-[500px] truncate"> + {row.getValue("name")} + </span> + </div> + ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + minSize: 200, + }, + { + accessorKey: "type", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 타입" /> + ), + cell: ({ row }) => { + const type = row.getValue("type") as string + return ( + <div className="flex w-[100px] items-center"> + <Badge variant="secondary"> + {type} + </Badge> + </div> + ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + minSize: 100, + }, + ] + + // 파일 정보 그룹 + groupMap["파일 정보"] = [ + { + accessorKey: "originalFileName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="GTC 파일" /> + ), + cell: ({ row }) => { + const fileName = row.getValue("originalFileName") as string | null + const filePath = row.original.filePath + + if (!fileName) { + return ( + <div className="flex items-center text-muted-foreground"> + <FileText className="mr-2 h-4 w-4" /> + <span>파일 없음</span> + </div> + ) + } + + return ( + <div className="flex items-center space-x-2"> + <FileText className="h-4 w-4" /> + <div className="flex flex-col"> + {filePath ? ( + <button + onClick={async (e) => { + e.preventDefault(); + e.stopPropagation(); + try { + // API를 통해 파일 다운로드 + const response = await fetch(`/api/project-gtc?action=download&projectId=${row.original.id}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('파일을 열 수 없습니다.'); + } + + // 파일 blob 생성 + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + window.open(url, '_blank'); + } catch (error) { + console.error("파일 미리보기 오류:", error); + toast.error("파일을 열 수 없습니다."); + } + }} + className="font-medium text-left hover:underline cursor-pointer text-blue-600 hover:text-blue-800 transition-colors" + title="클릭하여 파일 열기" + > + {fileName} + </button> + ) : ( + <span className="font-medium">{fileName}</span> + )} + </div> + </div> + ) + }, + minSize: 200, + }, + ] + + // 날짜 정보 그룹 + groupMap["날짜 정보"] = [ + { + accessorKey: "gtcCreatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="GTC 등록일" /> + ), + cell: ({ row }) => { + const date = row.getValue("gtcCreatedAt") as Date | null + if (!date) { + return <span className="text-muted-foreground">-</span> + } + return ( + <div className="flex items-center"> + <span> + {formatDateTime(new Date(date))} + </span> + </div> + ) + }, + minSize: 150, + }, + { + accessorKey: "projectCreatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 생성일" /> + ), + cell: ({ row }) => { + const date = row.getValue("projectCreatedAt") as Date + return ( + <div className="flex items-center"> + <span> + {formatDate(new Date(date))} + </span> + </div> + ) + }, + minSize: 120, + }, + ] + + // ---------------------------------------------------------------- + // 4-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 + // ---------------------------------------------------------------- + const nestedColumns: ColumnDef<ProjectGtcView>[] = [] + + // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함 + Object.entries(groupMap).forEach(([groupName, colDefs]) => { + // 상위 컬럼 + nestedColumns.push({ + id: groupName, + header: groupName, // "프로젝트 정보", "파일 정보", "날짜 정보" 등 + columns: colDefs, + }) + }) + + // ---------------------------------------------------------------- + // 5) 최종 컬럼 배열: select, download, nestedColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + downloadColumn, // 다운로드 컬럼 추가 + ...nestedColumns, + actionsColumn, + ] +}
\ No newline at end of file |
