"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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header" import { useRouter } from "next/navigation" import { VendorWithTypeAndMaterials, 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: 50, minSize: 50, maxSize: 50, 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/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 any, userId, vendorName: row.original.vendorName, // Required field from UpdateVendorSchema comment: `Status changed to ${value}` } as any), { loading: "Updating...", success: "Label updated", error: (err) => getErrorMessage(err), } ) }) }} > {vendors.status.enumValues.map((status) => ( {status} ))} ) }, size: 60, minSize: 60, maxSize: 60, } // ---------------------------------------------------------------- // 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 === "vendorClassification" ? undefined : cfg.id, id: cfg.id as string, enableResizing: true, header: ({ column }) => ( ), meta: { excelHeader: cfg.excelHeader, group: cfg.group, type: cfg.type, }, size: cfg.width || 150, minSize: cfg.minWidth || 100, maxSize: cfg.maxWidth, 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} ) : ( 미지정 ); } // 업체분류 컬럼 처리 - status 값에 따라 업체분류 결정 if (cfg.id === "vendorClassification") { const statusVal = row.original.status as StatusType; // status 값에 따른 업체분류 매핑 const getVendorClassification = (status: StatusType): string => { const classificationMap: Record = { "PENDING_REVIEW": "발굴업체", // 업체발굴 "REJECTED": "발굴업체", // 가입거절 "APPROVED": "잠재업체", // 가입승인 "IN_PQ": "잠재업체", // PQ요청 "PQ_SUBMITTED": "잠재업체", // PQ제출 "PQ_FAILED": "잠재업체", // 실사실패 "PQ_APPROVED": "잠재업체", // 실사통과 "IN_REVIEW": "잠재업체", // 정규등록검토 "READY_TO_SEND": "잠재업체", // 정규등록검토 "ACTIVE": "정규업체", // 정규등록 "INACTIVE": "중지업체", // 비활성화 "BLACKLISTED": "중지업체", // 거래금지 }; return classificationMap[status] || "미분류"; }; const classification = getVendorClassification(statusVal); // 업체분류별 배지 스타일 const getBadgeStyle = (classification: string) => { switch (classification) { case "발굴업체": return "bg-orange-50 text-orange-700 border-orange-200"; case "잠재업체": return "bg-blue-50 text-blue-700 border-blue-200"; case "정규업체": return "bg-green-50 text-green-700 border-green-200"; case "중지업체": return "bg-red-50 text-red-700 border-red-200"; case "폐기업체": return "bg-gray-50 text-gray-700 border-gray-200"; default: return "bg-gray-50 text-gray-700 border-gray-200"; } }; return ( {classification} ); } // 업체 분류 컬럼 처리 (별도로 표시하고 싶은 경우) 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") } // 업체대표품목 컬럼들 처리 if (cfg.id === "primaryMaterial1" || cfg.id === "primaryMaterial2" || cfg.id === "primaryMaterial3") { const materialVal = cell.getValue() as string | null; if (!materialVal) return -; return (
{materialVal}
); } // 성조회가입여부 처리 - 읽기전용 배지만 표시 (편집은 update-vendor-sheet에서 처리) if (cfg.id === "isAssociationMember") { const memberVal = row.original.isAssociationMember as string | null; const getDisplayText = (value: string | null) => { switch (value) { case "Y": return "가입"; case "N": return "미가입"; case "E": return "해당없음"; default: return "정보없음"; } }; const getBadgeStyle = (value: string | null) => { switch (value) { case "Y": return "bg-green-100 text-green-800 border-green-300"; case "N": return "bg-red-100 text-red-800 border-red-300"; case "E": return "bg-gray-100 text-gray-800 border-gray-300"; default: return "bg-gray-100 text-gray-800 border-gray-300"; } }; // 읽기전용 배지만 표시 return ( {getDisplayText(memberVal)} ); } // 최근 발주 실적 컬럼들 처리 if (cfg.id === "recentPoNumber") { const poNumber = cell.getValue() as string | null; if (!poNumber) return -; return (
{poNumber}
); } if (cfg.id === "recentPoOrderBy") { const orderBy = cell.getValue() as string | null; if (!orderBy) return -; return (
{orderBy}
); } if (cfg.id === "recentPoDate") { const poDate = cell.getValue() as Date | null; if (!poDate) return -; return (
{formatDate(poDate, "KR")}
); } // 정규업체등록현황 컬럼 - status 필드 표시 if (cfg.id === "regularVendorRegistration") { const statusVal = row.original.status as StatusType; if (!statusVal) return -; // status 값을 한글로 변환하여 표시 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": "거래 금지" // 구매가 제공한 화면정의서 상태로 텍스트 변경, 세분화 필요 "PENDING_REVIEW": "업체발굴", "APPROVED": "가입승인", "REJECTED": "가입거절", "IN_PQ": "PQ요청", "PQ_SUBMITTED": "PQ제출", "PQ_FAILED": "PQ실패", "PQ_APPROVED": "PQ 통과", "IN_REVIEW": "정규등록검토", "READY_TO_SEND": "정규등록검토", "ACTIVE": "정규등록", "INACTIVE": "비활성화", "BLACKLISTED": "거래금지" }; return statusMap[status] || status; }; return ( {getStatusDisplay(statusVal)} ); } // TODO 컬럼들 (UI만) - 모두 "-" 표시 if (cfg.id === "regularEvaluationGrade" || cfg.id === "faContract" || cfg.id === "avlRegistration" || cfg.id === "recentDeliveryNumber" || cfg.id === "recentDeliveryBy") { return -; } // 날짜 컬럼들 (TODO) if (cfg.id === "recentDeliveryDate") { return -; } // code etc... return row.getValue(cfg.id) ?? "" }, } 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, size: 50, minSize: 50, maxSize: 50, }; // ---------------------------------------------------------------- // 4) 최종 컬럼 배열: select, nestedColumns, actions // ---------------------------------------------------------------- return [ selectColumn, attachmentsColumn, ...nestedColumns, actionsColumn, ] }