summaryrefslogtreecommitdiff
path: root/lib/general-contract-template/template/general-contract-template-columns.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-12 08:01:02 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-12 08:01:02 +0000
commita9575387c3a765a1a65ebc179dae16a21af6eb25 (patch)
tree347f2b17b07039080fb2f116460004ba0b75a779 /lib/general-contract-template/template/general-contract-template-columns.tsx
parent47e527f5f763658600696ee58451fb666e692f5a (diff)
(임수민) 일반 계약 템플릿 구현 및 basic contract 필터 수정
Diffstat (limited to 'lib/general-contract-template/template/general-contract-template-columns.tsx')
-rw-r--r--lib/general-contract-template/template/general-contract-template-columns.tsx698
1 files changed, 698 insertions, 0 deletions
diff --git a/lib/general-contract-template/template/general-contract-template-columns.tsx b/lib/general-contract-template/template/general-contract-template-columns.tsx
new file mode 100644
index 00000000..e4167839
--- /dev/null
+++ b/lib/general-contract-template/template/general-contract-template-columns.tsx
@@ -0,0 +1,698 @@
+"use client"
+
+import * as React from "react"
+import { type DataTableRowAction } from "@/types/table"
+import { type ColumnDef } from "@tanstack/react-table"
+import { Download, Ellipsis, Paperclip, CheckCircle, XCircle, Eye, Copy, GitBranch } from "lucide-react"
+import { toast } from "sonner"
+
+import { getErrorMessage } from "@/lib/handle-error"
+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 { users } from "@/db/schema"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { GeneralContractTemplate } from "@/db/schema"
+import { quickDownload } from "@/lib/file-download"
+import { useRouter } from "next/navigation"
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<GeneralContractTemplate> | null>>
+ router: ReturnType<typeof useRouter>
+}
+
+/**
+ * 파일 다운로드 함수 (공용 유틸리티 사용)
+ */
+const handleFileDownload = async (filePath: string, fileName: string) => {
+ try {
+ await quickDownload(filePath, fileName);
+ } catch (error) {
+ console.error("파일 다운로드 오류:", error);
+ toast.error("파일 다운로드 중 오류가 발생했습니다.");
+ }
+};
+
+/**
+ * tanstack table 컬럼 정의 (중첩 헤더 버전)
+ */
+export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef<GeneralContractTemplate>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<GeneralContractTemplate> = {
+ 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"
+ />
+ ),
+ size: 30,
+ minSize: 30,
+ maxSize: 30,
+ enableSorting: false,
+ enableHiding: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) 파일 다운로드 컬럼 (아이콘)
+ // ----------------------------------------------------------------
+ const downloadColumn: ColumnDef<GeneralContractTemplate> = {
+ id: "download",
+ header: "",
+ cell: ({ row }) => {
+ const template = row.original;
+
+ return (
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={() => handleFileDownload(template.filePath || "", template.fileName || "")}
+ title={`${template.fileName} 다운로드`}
+ className="hover:bg-muted"
+ >
+ <Paperclip className="h-4 w-4" />
+ <span className="sr-only">다운로드</span>
+ </Button>
+ );
+ },
+ size: 30,
+ minSize: 30,
+ maxSize: 30,
+ enableSorting: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 3) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<GeneralContractTemplate> = {
+ id: "actions",
+ header: "",
+ enableHiding: false,
+ cell: function Cell({ row }) {
+ const [isUpdatePending, startUpdateTransition] = React.useTransition()
+ const template = row.original;
+
+ const handleViewDetails = () => {
+ router.push(`/evcp/general-contract-template/${template.id}`);
+ };
+
+ 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-44">
+ <DropdownMenuItem
+ onSelect={() => router.push(`/evcp/general-contract-template/${template.id}`)}
+ >
+ {/* <Eye className="mr-2 h-4 w-4" /> */}
+ 상세보기
+ </DropdownMenuItem>
+
+ <DropdownMenuSeparator />
+
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "create-revision" })}
+ >
+ {/* <GitBranch className="mr-2 h-4 w-4" /> */}
+ 리비전 생성하기
+ </DropdownMenuItem>
+
+ <DropdownMenuSeparator />
+
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "update" })}
+ >
+ 수정하기
+ </DropdownMenuItem>
+
+ {template.status === 'ACTIVE' && (
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "dispose" })}
+ >
+ 폐기하기
+ </DropdownMenuItem>
+ )}
+
+ {template.status === 'DISPOSED' && template.disposedAt && (
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "restore" })}
+ >
+ 복구하기
+ </DropdownMenuItem>
+ )}
+
+ <DropdownMenuSeparator />
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "delete" })}
+ >
+ 삭제하기
+ {/* <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> */}
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ size: 30,
+ minSize: 30,
+ maxSize: 30,
+ }
+
+ // ----------------------------------------------------------------
+ // 4) 컬럼 정의
+ // ----------------------------------------------------------------
+ const basicInfoColumns: ColumnDef<GeneralContractTemplate>[] = [
+ {
+ accessorKey: "id",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="No." />,
+ cell: ({ row }) => {
+ // Sequential number based on table order
+ return (
+ <div className="w-[60px] text-center">
+ {row.index + 1}
+ </div>
+ );
+ },
+ size: 60,
+ minSize: 50,
+ maxSize: 80,
+ enableSorting: false,
+ },
+ {
+ accessorKey: "status",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="상태" />,
+ cell: ({ row }) => {
+ const status = row.getValue("status") as string;
+ const getStatusDisplay = (status: string) => {
+ switch (status) {
+ case "ACTIVE":
+ return "A";
+ case "DISPOSED":
+ return "D";
+ case "INACTIVE":
+ return ""; // 빈 문자열로 "Null" 표현
+ default:
+ return "";
+ }
+ };
+ return (
+ <div className="w-[40px] text-center">
+ <span>{getStatusDisplay(status)}</span>
+ </div>
+ );
+ },
+ size: 80,
+ minSize: 60,
+ maxSize: 120,
+ enableResizing: true,
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id));
+ },
+ },
+ {
+ accessorKey: "contractTemplateType",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약종류" />,
+ cell: ({ row }) => {
+ const contractType = row.getValue("contractTemplateType") as string;
+ return (
+ <div className="w-[80px] text-center font-mono text-sm">
+ {contractType}
+ </div>
+ );
+ },
+ size: 100,
+ minSize: 80,
+ maxSize: 150,
+ enableResizing: true,
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id));
+ },
+ },
+
+ {
+ accessorKey: "contractTemplateName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약문서명" />,
+ cell: ({ row }) => {
+ const template = row.original;
+
+ const handleClick = () => {
+ router.push(`/evcp/general-contract-template/${template.id}`);
+ };
+
+ return (
+ <div className="flex flex-col min-w-0">
+ <button
+ onClick={handleClick}
+ className="truncate text-left hover:text-blue-600 hover:underline cursor-pointer transition-colors"
+ title="클릭하여 상세보기"
+ >
+ {template.contractTemplateName}
+ </button>
+ </div>
+ );
+ },
+ size: 250,
+ minSize: 200,
+ maxSize: 400,
+ enableResizing: true,
+ },
+ {
+ accessorKey: "revision",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="Rev." />,
+ cell: ({ row }) => {
+ const revision = row.getValue("revision") as number;
+ return (
+ <div className="w-[60px] text-center">
+ {revision}
+ </div>
+ );
+ },
+ size: 80,
+ minSize: 60,
+ maxSize: 120,
+ enableResizing: true,
+ },
+ ];
+
+ // const scopeColumns: ColumnDef<BasicContractTemplate>[] = [
+ // {
+ // accessorKey: "shipBuildingApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="조선해양" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("shipBuildingApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 80,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "windApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="풍력" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("windApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 60,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "pcApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="PC" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("pcApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 50,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "nbApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="NB" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("nbApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 50,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "rcApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="RC" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("rcApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 50,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "gyApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="GY" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("gyApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 50,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "sysApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="S&Sys" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("sysApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 60,
+ // enableResizing: true,
+ // },
+ // {
+ // accessorKey: "infraApplicable",
+ // header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="Infra" />,
+ // cell: ({ row }) => {
+ // const applicable = row.getValue("infraApplicable") as boolean;
+ // return (
+ // <div className="flex justify-center">
+ // {applicable ? (
+ // <CheckCircle className="h-4 w-4 text-green-500" />
+ // ) : (
+ // <XCircle className="h-4 w-4 text-gray-300" />
+ // )}
+ // </div>
+ // );
+ // },
+ // size: 60,
+ // enableResizing: true,
+ // },
+ // ];
+
+ const fileInfoColumns: ColumnDef<GeneralContractTemplate>[] = [
+ {
+ accessorKey: "fileName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="파일명" />,
+ cell: ({ row }) => {
+ const fileName = row.getValue("fileName") as string;
+ return (
+ <div className="min-w-0 max-w-full">
+ <span className="block truncate" title={fileName}>
+ {fileName}
+ </span>
+ </div>
+ );
+ },
+ size: 200,
+ minSize: 150,
+ maxSize: 300,
+ enableResizing: true,
+ },
+ ];
+
+ const auditColumns: ColumnDef<GeneralContractTemplate>[] = [
+ {
+ accessorKey: "createdAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="생성일" />,
+ cell: ({ row }) => {
+ const date = row.getValue("createdAt") as Date;
+ return date ? formatDateTime(date, "ko-KR") : "-";
+ },
+ size: 120,
+ minSize: 100,
+ maxSize: 180,
+ enableResizing: true,
+ },
+ {
+ accessorKey: "updatedAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="수정일" />,
+ cell: ({ row }) => {
+ const date = row.getValue("updatedAt") as Date;
+ return date ? formatDateTime(date, "ko-KR") : "-";
+ },
+ size: 120,
+ minSize: 100,
+ maxSize: 180,
+ enableResizing: true,
+ },
+ {
+ accessorKey: "disposedAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="폐기일" />,
+ cell: ({ row }) => {
+ const date = row.getValue("disposedAt") as Date;
+ return date ? formatDateTime(date, "ko-KR") : "-";
+ },
+ size: 120,
+ minSize: 100,
+ maxSize: 180,
+ enableResizing: true,
+ },
+ {
+ accessorKey: "restoredAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="복구일" />,
+ cell: ({ row }) => {
+ const date = row.getValue("restoredAt") as Date;
+ return date ? formatDateTime(date, "ko-KR") : "-";
+ },
+ size: 120,
+ minSize: 100,
+ maxSize: 180,
+ enableResizing: true,
+ },
+ ];
+
+ // ----------------------------------------------------------------
+ // 5) 최종 컬럼 배열: 사용자 요구사항 순서에 맞게 재배치
+ // ----------------------------------------------------------------
+ return [
+ selectColumn, // ㅁ* (체크박스)
+ { // No.
+ accessorKey: "id",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="No." />,
+ cell: ({ row }) => {
+ return (
+ <div className="w-[60px] text-center">
+ {row.index + 1}
+ </div>
+ );
+ },
+ size: 40,
+ minSize: 40,
+ maxSize: 70,
+ enableSorting: false,
+ },
+ { // 상태
+ accessorKey: "status",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="상태" />,
+ cell: ({ row }) => {
+ const status = row.getValue("status") as string;
+ let displayStatus = "";
+ if (status === "ACTIVE") displayStatus = "A";
+ else if (status === "DISPOSED") displayStatus = "D";
+ // INACTIVE는 빈 문자열로 "Null" 표현
+
+ return (
+ <div className="w-[50px] text-center font-medium">
+ {displayStatus}
+ </div>
+ );
+ },
+ size: 50,
+ minSize: 40,
+ maxSize: 70,
+ enableResizing: true,
+ },
+ { // 계약종류
+ accessorKey: "contractTemplateType",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약종류" />,
+ cell: ({ row }) => {
+ const contractType = row.getValue("contractTemplateType") as string;
+ return (
+ <div className="w-[80px] text-center font-medium">
+ {contractType}
+ </div>
+ );
+ },
+ size: 80,
+ minSize: 60,
+ maxSize: 120,
+ enableResizing: true,
+ },
+ { // 계약문서명
+ accessorKey: "contractTemplateName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="계약문서명" />,
+ cell: ({ row }) => {
+ const contractName = row.getValue("contractTemplateName") as string;
+ return (
+ <div className="max-w-[400px] truncate" title={contractName}>
+ {contractName}
+ </div>
+ );
+ },
+ size: 200,
+ minSize: 150,
+ maxSize: 1000,
+ enableResizing: true,
+ },
+ { // Rev.
+ accessorKey: "revision",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="Rev." />,
+ cell: ({ row }) => {
+ const revision = row.getValue("revision") as number;
+ return (
+ <div className="w-[60px] text-center">
+ {revision}
+ </div>
+ );
+ },
+ size: 60,
+ minSize: 50,
+ maxSize: 80,
+ enableResizing: true,
+ },
+ { // 법무검토
+ accessorKey: "legalReviewRequired",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="법무검토" />,
+ cell: ({ row }) => {
+ const required = row.getValue("legalReviewRequired") as boolean;
+ return (
+ <Badge variant={required ? "destructive" : "secondary"}>
+ {required ? "필요" : "불필요"}
+ </Badge>
+ );
+ },
+ size: 100,
+ minSize: 80,
+ maxSize: 150,
+ enableResizing: true,
+ },
+ { // 최종 Update일
+ accessorKey: "updatedAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종 Update일" />,
+ cell: ({ row }) => {
+ const date = row.getValue("updatedAt") as Date;
+ return date ? formatDate(date) : "";
+ },
+ size: 120,
+ minSize: 100,
+ maxSize: 180,
+ enableResizing: true,
+ },
+ { // 최종 Update자
+ accessorKey: "updatedByName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종 Update자" />,
+ cell: ({ row }) => {
+ const updatedByName = row.getValue("updatedByName") as string | null;
+ return updatedByName || "";
+ },
+ size: 120,
+ minSize: 100,
+ maxSize: 180,
+ enableResizing: true,
+ },
+ { // 폐기일자
+ accessorKey: "disposedAt",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="폐기일자" />,
+ cell: ({ row }) => {
+ const date = row.getValue("disposedAt") as Date | null;
+ return date ? formatDate(date) : "";
+ },
+ size: 120,
+ minSize: 100,
+ maxSize: 180,
+ enableResizing: true,
+ },
+ { // 첨부
+ accessorKey: "fileName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="첨부" />,
+ cell: ({ row }) => {
+ const template = row.original;
+
+ return (
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={() => handleFileDownload(template.filePath || "", template.fileName || "")}
+ title={`${template.fileName} 다운로드`}
+ className="hover:bg-muted"
+ >
+ <Paperclip className="h-4 w-4" />
+ <span className="sr-only">다운로드</span>
+ </Button>
+ );
+ },
+ maxSize: 50,
+ enableSorting: false,
+ },
+ actionsColumn, // 빈 컬럼 (···)
+ ]
+} \ No newline at end of file