summaryrefslogtreecommitdiff
path: root/lib/tech-vendors/table/tech-vendors-table-columns.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tech-vendors/table/tech-vendors-table-columns.tsx')
-rw-r--r--lib/tech-vendors/table/tech-vendors-table-columns.tsx788
1 files changed, 375 insertions, 413 deletions
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<typeof useRouter>;
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<TechVendor> | null>>;
- router: NextRouter;
- openItemsDialog: (vendor: TechVendor) => void;
-}
-
-
-
-
-/**
- * tanstack table 컬럼 정의 (중첩 헤더 버전)
- */
-export function getColumns({ setRowAction, router, openItemsDialog }: GetColumnsProps): ColumnDef<TechVendor>[] {
- // ----------------------------------------------------------------
- // 1) select 컬럼 (체크박스)
- // ----------------------------------------------------------------
- const selectColumn: ColumnDef<TechVendor> = {
- 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: 40,
- enableSorting: false,
- enableHiding: false,
- }
-
- // ----------------------------------------------------------------
- // 2) actions 컬럼 (Dropdown 메뉴)
- // ----------------------------------------------------------------
- const actionsColumn: ColumnDef<TechVendor> = {
- id: "actions",
- enableHiding: false,
- cell: function Cell({ row }) {
- const [isUpdatePending, startUpdateTransition] = React.useTransition()
-
- 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-56">
- <DropdownMenuItem
- onSelect={() => setRowAction({ row, type: "update" })}
- >
- 레코드 편집
- </DropdownMenuItem>
-
- <DropdownMenuItem
- onSelect={() => {
- // 1) 만약 rowAction을 열고 싶다면
- // setRowAction({ row, type: "update" })
-
- // 2) 자세히 보기 페이지로 클라이언트 라우팅
- router.push(`/evcp/tech-vendors/${row.original.id}/info`);
- }}
- >
- 상세보기
- </DropdownMenuItem>
- <DropdownMenuItem
- onSelect={() => {
- // 새창으로 열기 위해 window.open() 사용
- window.open(`/evcp/tech-vendors/${row.original.id}/info`, '_blank');
- }}
- >
- 상세보기(새창)
- </DropdownMenuItem>
-
- <Separator />
- <DropdownMenuSub>
- <DropdownMenuSubTrigger>Status</DropdownMenuSubTrigger>
- <DropdownMenuSubContent>
- <DropdownMenuRadioGroup
- value={row.original.status}
- onValueChange={(value) => {
- 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) => (
- <DropdownMenuRadioItem
- key={status}
- value={status}
- className="capitalize"
- disabled={isUpdatePending}
- >
- {status}
- </DropdownMenuRadioItem>
- ))}
- </DropdownMenuRadioGroup>
- </DropdownMenuSubContent>
- </DropdownMenuSub>
-
-
- </DropdownMenuContent>
- </DropdownMenu>
- )
- },
- size: 40,
- }
-
- // ----------------------------------------------------------------
- // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성
- // ----------------------------------------------------------------
- // 3-1) groupMap: { [groupName]: ColumnDef<TechVendor>[] }
- const groupMap: Record<string, ColumnDef<TechVendor>[]> = {}
-
- techVendorColumnsConfig.forEach((cfg) => {
- // 만약 group가 없으면 "_noGroup" 처리
- const groupName = cfg.group || "_noGroup"
-
- if (!groupMap[groupName]) {
- groupMap[groupName] = []
- }
-
- // child column 정의
- const childCol: ColumnDef<TechVendor> = {
- accessorKey: cfg.id,
- enableResizing: true,
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={cfg.label} />
- ),
- 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 (
- <div className="flex items-center gap-2">
- <Badge
- variant={statusConfig.variant}
- className={statusConfig.className}
- >
- <StatusIcon className={`mr-1 h-3.5 w-3.5 ${statusConfig.iconColor}`} />
- {displayText}
- </Badge>
- </div>
- );
- }
- // 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 (
- <div className="flex flex-wrap gap-1">
- {types.length > 0 ? types.map((type, index) => (
- <Badge key={`${type}-${index}`} variant="secondary" className="text-xs">
- {type}
- </Badge>
- )) : (
- <span className="text-muted-foreground">-</span>
- )}
- </div>
- );
- }
-
- // 날짜 컬럼 포맷팅
- 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<TechVendor>[] = [
- 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<TechVendor> = {
- id: "possibleItems",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="자재 그룹" />
- ),
- cell: ({ row }) => {
- const vendor = row.original;
-
- const handleClick = () => {
- openItemsDialog(vendor);
- };
-
- return (
- <Button
- variant="ghost"
- size="sm"
- className="relative h-8 w-8 p-0 group"
- onClick={handleClick}
- aria-label="View possible items"
- >
- <Package className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
- <span className="sr-only">
- Possible Items 보기
- </span>
- </Button>
- );
- },
- 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<typeof useRouter>;
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<TechVendor> | null>>;
+ router: NextRouter;
+}
+
+
+
+
+/**
+ * tanstack table 컬럼 정의 (중첩 헤더 버전)
+ */
+export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef<TechVendor>[] {
+ // ----------------------------------------------------------------
+ // 1) select 컬럼 (체크박스)
+ // ----------------------------------------------------------------
+ const selectColumn: ColumnDef<TechVendor> = {
+ 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: 40,
+ enableSorting: false,
+ enableHiding: false,
+ }
+
+ // ----------------------------------------------------------------
+ // 2) actions 컬럼 (Dropdown 메뉴)
+ // ----------------------------------------------------------------
+ const actionsColumn: ColumnDef<TechVendor> = {
+ id: "actions",
+ enableHiding: false,
+ cell: function Cell({ row }) {
+ const [isUpdatePending, startUpdateTransition] = React.useTransition()
+
+ 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-56">
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "update" })}
+ >
+ 레코드 편집
+ </DropdownMenuItem>
+
+ <DropdownMenuItem
+ onSelect={() => {
+ // 1) 만약 rowAction을 열고 싶다면
+ // setRowAction({ row, type: "update" })
+
+ // 2) 자세히 보기 페이지로 클라이언트 라우팅
+ router.push(`/evcp/tech-vendors/${row.original.id}/info`);
+ }}
+ >
+ 상세보기
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ onSelect={() => {
+ // 새창으로 열기 위해 window.open() 사용
+ window.open(`/evcp/tech-vendors/${row.original.id}/info`, '_blank');
+ }}
+ >
+ 상세보기(새창)
+ </DropdownMenuItem>
+
+ <Separator />
+ <DropdownMenuSub>
+ <DropdownMenuSubTrigger>Status</DropdownMenuSubTrigger>
+ <DropdownMenuSubContent>
+ <DropdownMenuRadioGroup
+ value={row.original.status}
+ onValueChange={(value) => {
+ 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) => (
+ <DropdownMenuRadioItem
+ key={status}
+ value={status}
+ className="capitalize"
+ disabled={isUpdatePending}
+ >
+ {status}
+ </DropdownMenuRadioItem>
+ ))}
+ </DropdownMenuRadioGroup>
+ </DropdownMenuSubContent>
+ </DropdownMenuSub>
+
+
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ size: 40,
+ }
+
+ // ----------------------------------------------------------------
+ // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성
+ // ----------------------------------------------------------------
+ // 3-1) groupMap: { [groupName]: ColumnDef<TechVendor>[] }
+ const groupMap: Record<string, ColumnDef<TechVendor>[]> = {}
+
+ techVendorColumnsConfig.forEach((cfg) => {
+ // 만약 group가 없으면 "_noGroup" 처리
+ const groupName = cfg.group || "_noGroup"
+
+ if (!groupMap[groupName]) {
+ groupMap[groupName] = []
+ }
+
+ // child column 정의
+ const childCol: ColumnDef<TechVendor> = {
+ accessorKey: cfg.id,
+ enableResizing: true,
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title={cfg.label} />
+ ),
+ 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 (
+ <div className="flex items-center gap-2">
+ <Badge
+ variant={statusConfig.variant}
+ className={statusConfig.className}
+ >
+ <StatusIcon className={`mr-1 h-3.5 w-3.5 ${statusConfig.iconColor}`} />
+ {displayText}
+ </Badge>
+ </div>
+ );
+ }
+ // 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 (
+ <div className="flex flex-wrap gap-1">
+ {types.length > 0 ? types.map((type, index) => (
+ <Badge key={`${type}-${index}`} variant="secondary" className="text-xs">
+ {type}
+ </Badge>
+ )) : (
+ <span className="text-muted-foreground">-</span>
+ )}
+ </div>
+ );
+ }
+
+ // 날짜 컬럼 포맷팅
+ 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<TechVendor>[] = [
+ 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