"use client" import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" import { Ellipsis } 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, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { useRouter } from "next/navigation" import { TechVendor, techVendors } from "@/db/schema/techVendors" import { modifyTechVendor } from "../service" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { techVendorColumnsConfig } from "@/config/techVendorColumnsConfig" import { Separator } from "@/components/ui/separator" import { getVendorStatusIcon } from "../utils" import { DeleteTechVendorDialog } from "./delete-tech-vendors-dialog" // 타입 정의 추가 type StatusType = (typeof techVendors.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; onVendorDeleted?: () => void; } /** * tanstack table 컬럼 정의 (중첩 헤더 버전) */ export function getColumns({ setRowAction, router, onVendorDeleted }: 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() return ( setRowAction({ row, type: "update" })} > 레코드 편집 { // 1) 만약 rowAction을 열고 싶다면 // setRowAction({ row, type: "update" }) // 2) 자세히 보기 페이지로 클라이언트 라우팅 router.push(`/evcp/tech-vendors/${row.original.id}/info`); }} > 상세보기 { // 새창으로 열기 위해 window.open() 사용 window.open(`/evcp/tech-vendors/${row.original.id}/info`, '_blank'); }} > 상세보기(새창) Status { startUpdateTransition(() => { toast.promise( modifyTechVendor({ id: String(row.original.id), status: value as TechVendor["status"], vendorName: row.original.vendorName, // Required field from UpdateVendorSchema }), { loading: "Updating...", success: "Label updated", error: (err) => getErrorMessage(err), } ) }) }} > {techVendors.status.enumValues.map((status) => ( {status} ))} {/* 벤더 임시 삭제 버튼 */}
) }, size: 40, } // ---------------------------------------------------------------- // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 // ---------------------------------------------------------------- // 3-1) groupMap: { [groupName]: ColumnDef[] } const groupMap: Record[]> = {} techVendorColumnsConfig.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; if (!statusVal) return null; // Status badge variant mapping - 더 뚜렷한 색상으로 변경 const getStatusConfig = (status: StatusType): StatusConfig & { iconColor: string } => { switch (status) { case "ACTIVE": return { variant: "default", className: "bg-emerald-100 text-emerald-800 border-emerald-300 font-semibold", iconColor: "text-emerald-600" }; case "INACTIVE": return { variant: "default", className: "bg-gray-100 text-gray-800 border-gray-300", iconColor: "text-gray-600" }; case "PENDING_INVITE": return { variant: "default", className: "bg-blue-100 text-blue-800 border-blue-300", iconColor: "text-blue-600" }; case "INVITED": return { variant: "default", className: "bg-green-100 text-green-800 border-green-300", iconColor: "text-green-600" }; case "QUOTE_COMPARISON": return { variant: "default", className: "bg-purple-100 text-purple-800 border-purple-300", iconColor: "text-purple-600" }; case "BLACKLISTED": return { variant: "destructive", className: "bg-slate-800 text-white border-slate-900", iconColor: "text-white" }; default: return { variant: "default", className: "bg-gray-100 text-gray-800 border-gray-300", iconColor: "text-gray-600" }; } }; // 상태 표시 텍스트 const getStatusDisplay = (status: StatusType): string => { const statusMap: StatusDisplayMap = { "ACTIVE": "활성 상태", "INACTIVE": "비활성 상태", "BLACKLISTED": "거래 금지", "PENDING_INVITE": "초대 대기", "INVITED": "초대 완료", "QUOTE_COMPARISON": "견적 비교" }; return statusMap[status] || status; }; const statusConfig = getStatusConfig(statusVal); const displayText = getStatusDisplay(statusVal); const StatusIcon = getVendorStatusIcon(statusVal); return (
{displayText}
); } // TechVendorType 컬럼을 badge로 표시 if (cfg.id === "techVendorType") { const techVendorType = row.original.techVendorType as string | null | undefined; // 벤더 타입 파싱 개선 - null/undefined 안전 처리 let types: string[] = []; if (!techVendorType) { types = []; } else if (techVendorType.startsWith('[') && techVendorType.endsWith(']')) { // JSON 배열 형태 try { const parsed = JSON.parse(techVendorType); types = Array.isArray(parsed) ? parsed.filter(Boolean) : [techVendorType]; } catch { types = [techVendorType]; } } else if (techVendorType.includes(',')) { // 콤마로 구분된 문자열 types = techVendorType.split(',').map(t => t.trim()).filter(Boolean); } else { // 단일 문자열 types = [techVendorType.trim()].filter(Boolean); } // 벤더 타입 정렬 - 조선 > 해양top > 해양hull 순 const typeOrder = ["조선", "해양top", "해양hull"]; types.sort((a, b) => { const indexA = typeOrder.indexOf(a); const indexB = typeOrder.indexOf(b); // 정의된 순서에 있는 경우 우선순위 적용 if (indexA !== -1 && indexB !== -1) { return indexA - indexB; } return a.localeCompare(b); }); return (
{types.length > 0 ? types.map((type, index) => ( {type} )) : ( - )}
); } // 날짜 컬럼 포맷팅 if (cfg.type === "date" && cell.getValue()) { return formatDate(cell.getValue() as Date, "ko-KR"); } return cell.getValue(); }, }; groupMap[groupName].push(childCol); }); // 3-2) groupMap -> columns (그룹별 -> 중첩 헤더 ColumnDef[] 배열 변환) const columns: ColumnDef[] = [ selectColumn, // 1) 체크박스 ]; // 3-3) 그룹이 있는 컬럼들은 중첩 헤더로, 없는 것들은 그냥 컬럼으로 Object.entries(groupMap).forEach(([groupName, childColumns]) => { if (groupName === "_noGroup") { // 그룹이 없는 컬럼들은 그냥 추가 columns.push(...childColumns); } else { // 그룹이 있는 컬럼들은 헤더 아래 자식으로 중첩 columns.push({ id: groupName, header: groupName, // 그룹명을 헤더로 columns: childColumns, // 그룹에 속한 컬럼들을 자식으로 }); } }); columns.push(actionsColumn); // 마지막에 액션 컬럼 추가 return columns; }