"use client" import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" import { Ellipsis, PaperclipIcon } from "lucide-react" import { toast } from "sonner" import { getErrorMessage } from "@/lib/handle-error" import { formatDate } 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, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header" import { useRouter } from "next/navigation" import { VendorWithType, vendors, VendorWithAttachments } from "@/db/schema/vendors" import { modifyVendor } from "../service" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { vendorColumnsConfig } from "@/config/vendorColumnsConfig" import { Separator } from "@/components/ui/separator" import { AttachmentsButton } from "./attachmentButton" import { getVendorStatusIcon } from "../utils" // 타입 정의 추가 type StatusType = (typeof vendors.status.enumValues)[number]; type BadgeVariantType = "default" | "secondary" | "destructive" | "outline"; type StatusConfig = { variant: BadgeVariantType; className: string; }; type StatusDisplayMap = { [key in StatusType]: string; }; type NextRouter = ReturnType; interface GetColumnsProps { setRowAction: React.Dispatch | null>>; router: NextRouter; userId: number; } /** * tanstack table 컬럼 정의 (중첩 헤더 버전) */ export function getColumns({ setRowAction, router, userId }: GetColumnsProps): ColumnDef[] { // ---------------------------------------------------------------- // 1) select 컬럼 (체크박스) // ---------------------------------------------------------------- const selectColumn: ColumnDef = { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" className="translate-y-0.5" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="Select row" className="translate-y-0.5" /> ), size: 40, enableSorting: false, enableHiding: false, } // ---------------------------------------------------------------- // 2) actions 컬럼 (Dropdown 메뉴) // ---------------------------------------------------------------- const actionsColumn: ColumnDef = { id: "actions", enableHiding: false, cell: function Cell({ row }) { const [isUpdatePending, startUpdateTransition] = React.useTransition() const isApproved = row.original.status === "PQ_APPROVED"; const afterApproved = row.original.status === "ACTIVE"; return ( {(isApproved || afterApproved) && ( setRowAction({ row, type: "update" })} > 레코드 편집 )} { // 1) 만약 rowAction을 열고 싶다면 // setRowAction({ row, type: "update" }) // 2) 자세히 보기 페이지로 클라이언트 라우팅 router.push(`/evcp/vendors/${row.original.id}/info`); }} > 상세보기 { // 새창으로 열기 위해 window.open() 사용 window.open(`/evcp/vendors/${row.original.id}/info`, '_blank'); }} > 상세보기(새창) setRowAction({ row, type: "log" })} > 감사 로그 보기 Status { startUpdateTransition(() => { toast.promise( modifyVendor({ id: String(row.original.id), status: value as VendorWithType["status"], userId, vendorName: row.original.vendorName, // Required field from UpdateVendorSchema comment: `Status changed to ${value}` }), { loading: "Updating...", success: "Label updated", error: (err) => getErrorMessage(err), } ) }) }} > {vendors.status.enumValues.map((status) => ( {status} ))} ) }, size: 40, } // ---------------------------------------------------------------- // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 // ---------------------------------------------------------------- // 3-1) groupMap: { [groupName]: ColumnDef[] } const groupMap: Record[]> = {} vendorColumnsConfig.forEach((cfg) => { // 만약 group가 없으면 "_noGroup" 처리 const groupName = cfg.group || "_noGroup" if (!groupMap[groupName]) { groupMap[groupName] = [] } // child column 정의 const childCol: ColumnDef = { accessorKey: cfg.id, enableResizing: true, header: ({ column }) => ( ), meta: { excelHeader: cfg.excelHeader, group: cfg.group, type: cfg.type, }, cell: ({ row, cell }) => { // Status 컬럼 렌더링 개선 - 아이콘과 더 선명한 배경색 사용 if (cfg.id === "status") { const statusVal = row.original.status as StatusType; if (!statusVal) return null; // Status badge variant mapping - 더 뚜렷한 색상으로 변경 const getStatusConfig = (status: StatusType): StatusConfig & { iconColor: string } => { switch (status) { case "PENDING_REVIEW": return { variant: "outline", className: "bg-yellow-100 text-yellow-800 border-yellow-300", iconColor: "text-yellow-600" }; case "IN_REVIEW": return { variant: "outline", className: "bg-blue-100 text-blue-800 border-blue-300", iconColor: "text-blue-600" }; case "REJECTED": return { variant: "outline", className: "bg-red-100 text-red-800 border-red-300", iconColor: "text-red-600" }; case "IN_PQ": return { variant: "outline", className: "bg-purple-100 text-purple-800 border-purple-300", iconColor: "text-purple-600" }; case "PQ_SUBMITTED": return { variant: "outline", className: "bg-indigo-100 text-indigo-800 border-indigo-300", iconColor: "text-indigo-600" }; case "PQ_FAILED": return { variant: "outline", className: "bg-red-100 text-red-800 border-red-300", iconColor: "text-red-600" }; case "PQ_APPROVED": return { variant: "outline", className: "bg-green-100 text-green-800 border-green-300", iconColor: "text-green-600" }; case "APPROVED": return { variant: "outline", className: "bg-green-100 text-green-800 border-green-300", iconColor: "text-green-600" }; case "READY_TO_SEND": return { variant: "outline", className: "bg-emerald-100 text-emerald-800 border-emerald-300", iconColor: "text-emerald-600" }; case "ACTIVE": return { variant: "outline", className: "bg-emerald-100 text-emerald-800 border-emerald-300 font-semibold", iconColor: "text-emerald-600" }; case "INACTIVE": return { variant: "outline", className: "bg-gray-100 text-gray-800 border-gray-300", iconColor: "text-gray-600" }; case "BLACKLISTED": return { variant: "outline", className: "bg-slate-800 text-white border-slate-900", iconColor: "text-white" }; default: return { variant: "outline", className: "bg-gray-100 text-gray-800 border-gray-300", iconColor: "text-gray-600" }; } }; // Translate status for display const getStatusDisplay = (status: StatusType): string => { const statusMap: StatusDisplayMap = { "PENDING_REVIEW": "가입 신청 중", "IN_REVIEW": "심사 중", "REJECTED": "심사 거부됨", "IN_PQ": "PQ 진행 중", "PQ_SUBMITTED": "PQ 제출", "PQ_FAILED": "PQ 실패", "PQ_APPROVED": "PQ 통과", "APPROVED": "승인됨", "READY_TO_SEND": "MDG 송부대기", "ACTIVE": "활성 상태", "INACTIVE": "비활성 상태", "BLACKLISTED": "거래 금지" }; return statusMap[status] || status; }; const config = getStatusConfig(statusVal); const displayText = getStatusDisplay(statusVal); const StatusIcon = getVendorStatusIcon(statusVal); return ( {displayText} ); } // 업체 유형 컬럼 처리 if (cfg.id === "vendorTypeName") { const typeVal = row.original.vendorTypeName as string | null; return typeVal ? ( {typeVal} ) : ( 미지정 ); } // 업체 분류 컬럼 처리 (별도로 표시하고 싶은 경우) if (cfg.id === "vendorCategory") { const categoryVal = row.original.vendorCategory as string | null; if (!categoryVal) return null; let badgeClass = ""; if (categoryVal === "정규업체") { badgeClass = "bg-green-50 text-green-700 border-green-200"; } else if (categoryVal === "잠재업체") { badgeClass = "bg-blue-50 text-blue-700 border-blue-200"; } return ( {categoryVal} ); } if (cfg.id === "createdAt") { const dateVal = cell.getValue() as Date return formatDate(dateVal, "KR") } if (cfg.id === "updatedAt") { const dateVal = cell.getValue() as Date return formatDate(dateVal, "KR") } // code etc... return row.getValue(cfg.id) ?? "" }, minSize: 150 } groupMap[groupName].push(childCol) }) // ---------------------------------------------------------------- // 3-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 // ---------------------------------------------------------------- const nestedColumns: ColumnDef[] = [] // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함 // 여기서는 그냥 Object.entries 순서 Object.entries(groupMap).forEach(([groupName, colDefs]) => { if (groupName === "_noGroup") { // 그룹 없음 → 그냥 최상위 레벨 컬럼 nestedColumns.push(...colDefs) } else { // 상위 컬럼 nestedColumns.push({ id: groupName, header: groupName, // "Basic Info", "Metadata" 등 columns: colDefs, }) } }) // attachments 컬럼 타입 문제 해결을 위한 타입 단언 const attachmentsColumn: ColumnDef = { id: "attachments", header: ({ column }) => ( ), cell: ({ row }) => { const vendor = row.original as unknown as VendorWithAttachments; // 속성이 undefined일 수 있으므로 옵셔널 체이닝과 기본값 사용 const hasAttachments = vendor.hasAttachments ?? false; const attachmentsList = vendor.attachmentsList ?? []; if (hasAttachments) { // 서버 액션을 사용하는 컴포넌트로 교체 return ( ); } else { return null; } }, enableSorting: false, enableHiding: false, minSize: 45, }; // ---------------------------------------------------------------- // 4) 최종 컬럼 배열: select, nestedColumns, actions // ---------------------------------------------------------------- return [ selectColumn, attachmentsColumn, ...nestedColumns, actionsColumn, ] }