From 14f61e24947fb92dd71ec0a7196a6e815f8e66da Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 21 Jul 2025 07:54:26 +0000 Subject: (최겸)기술영업 RFQ 담당자 초대, 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/tech-vendors-table-columns.tsx | 788 ++++++++++----------- 1 file changed, 375 insertions(+), 413 deletions(-) (limited to 'lib/tech-vendors/table/tech-vendors-table-columns.tsx') diff --git a/lib/tech-vendors/table/tech-vendors-table-columns.tsx b/lib/tech-vendors/table/tech-vendors-table-columns.tsx index 052794ce..5184e3f3 100644 --- a/lib/tech-vendors/table/tech-vendors-table-columns.tsx +++ b/lib/tech-vendors/table/tech-vendors-table-columns.tsx @@ -1,414 +1,376 @@ -"use client" - -import * as React from "react" -import { type DataTableRowAction } from "@/types/table" -import { type ColumnDef } from "@tanstack/react-table" -import { Ellipsis, Package } 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" - -// 타입 정의 추가 -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; - openItemsDialog: (vendor: TechVendor) => void; -} - - - - -/** - * tanstack table 컬럼 정의 (중첩 헤더 버전) - */ -export function getColumns({ setRowAction, router, openItemsDialog }: 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); - } - - 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, // 그룹에 속한 컬럼들을 자식으로 - }); - } - }); - - // Possible Items 컬럼 추가 (액션 컬럼 직전에) - const possibleItemsColumn: ColumnDef = { - id: "possibleItems", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const vendor = row.original; - - const handleClick = () => { - openItemsDialog(vendor); - }; - - return ( - - ); - }, - enableSorting: false, - enableResizing: false, - size: 80, - meta: { - excelHeader: "Possible Items" - }, - }; - - columns.push(possibleItemsColumn); - columns.push(actionsColumn); // 마지막에 액션 컬럼 추가 - - return columns; +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Ellipsis, Package } 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" + +// 타입 정의 추가 +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; +} + + + + +/** + * tanstack table 컬럼 정의 (중첩 헤더 버전) + */ +export function getColumns({ setRowAction, router }: 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); + } + + 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; } \ No newline at end of file -- cgit v1.2.3