diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 08:24:16 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 08:24:16 +0000 |
| commit | 44bdb81a60d3a44ba7e379f3c20fe6d8fb284339 (patch) | |
| tree | b5c916a1c7ea37573f9bba7fefcef60a3b8aec20 /lib/page-visits/table/page-visits-table-columns.tsx | |
| parent | 90f79a7a691943a496f67f01c1e493256070e4de (diff) | |
(대표님) 변경사항 20250707 12시 30분
Diffstat (limited to 'lib/page-visits/table/page-visits-table-columns.tsx')
| -rw-r--r-- | lib/page-visits/table/page-visits-table-columns.tsx | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/lib/page-visits/table/page-visits-table-columns.tsx b/lib/page-visits/table/page-visits-table-columns.tsx new file mode 100644 index 00000000..e1d2fed4 --- /dev/null +++ b/lib/page-visits/table/page-visits-table-columns.tsx @@ -0,0 +1,309 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" + +import { formatDate } from "@/lib/utils" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { ExtendedPageVisit } from "../validation" +import { Eye, ExternalLink, Clock, User, Ellipsis } from "lucide-react" + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ExtendedPageVisit> | null>> +} + +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ExtendedPageVisit>[] { + return [ + { + 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" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + id: "사용자", + header: "사용자", + columns: [ + { + accessorKey: "userEmail", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="사용자" /> + ), + cell: ({ row }) => { + const userEmail = row.getValue("userEmail") as string | null + const userName = row.original.userName + + if (!userEmail) { + return ( + <div className="flex items-center gap-2"> + <User className="size-3 text-muted-foreground" /> + <span className="text-muted-foreground text-xs">익명</span> + </div> + ) + } + + return ( + <div className="flex flex-col"> + <span className="font-medium text-sm">{userEmail}</span> + {userName && ( + <span className="text-xs text-muted-foreground">{userName}</span> + )} + </div> + ) + }, + }, + ], + }, + { + id: "페이지 정보", + header: "페이지 정보", + columns: [ + { + accessorKey: "route", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="경로" /> + ), + cell: ({ row }) => { + const route = row.getValue("route") as string + const pageTitle = row.original.pageTitle + + return ( + <div className="flex flex-col max-w-[200px]"> + <code className="text-xs bg-muted px-1 py-0.5 rounded font-mono"> + {route} + </code> + {pageTitle && ( + <span className="text-xs text-muted-foreground mt-1 truncate"> + {pageTitle} + </span> + )} + </div> + ) + }, + }, + { + accessorKey: "visitedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="방문 시간" /> + ), + cell: ({ row }) => { + const date = row.getValue("visitedAt") as Date + return ( + <Tooltip> + <TooltipTrigger> + <div className="text-sm"> + {formatDate(date, 'KR')} + </div> + </TooltipTrigger> + <TooltipContent> + {formatDate(date)} + </TooltipContent> + </Tooltip> + ) + }, + }, + { + accessorKey: "duration", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="체류 시간" /> + ), + cell: ({ row }) => { + const duration = row.getValue("duration") as number | null + const isLongVisit = row.original.isLongVisit + + if (!duration) { + return <span className="text-muted-foreground">-</span> + } + + const minutes = Math.floor(duration / 60) + const seconds = duration % 60 + + let displayText = "" + if (minutes > 0) { + displayText = `${minutes}분 ${seconds}초` + } else { + displayText = `${seconds}초` + } + + return ( + <div className="flex items-center gap-2"> + {isLongVisit && <Clock className="size-3 text-orange-500" />} + <span className={isLongVisit ? "font-medium" : ""}> + {displayText} + </span> + </div> + ) + }, + }, + ], + }, + { + id: "환경 정보", + header: "환경 정보", + columns: [ + { + accessorKey: "deviceType", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="디바이스" /> + ), + cell: ({ row }) => { + const deviceType = row.getValue("deviceType") as string + const variants = { + desktop: "default", + mobile: "secondary", + tablet: "outline", + } as const + + return ( + <Badge variant={variants[deviceType as keyof typeof variants] || "default"} className="text-xs"> + {deviceType} + </Badge> + ) + }, + }, + { + accessorKey: "browserName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="브라우저" /> + ), + cell: ({ row }) => { + const browserName = row.getValue("browserName") as string | null + const osName = row.original.osName + + return ( + <div className="flex flex-col"> + <span className="text-sm">{browserName || "Unknown"}</span> + {osName && ( + <span className="text-xs text-muted-foreground">{osName}</span> + )} + </div> + ) + }, + }, + { + accessorKey: "ipAddress", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="IP" /> + ), + cell: ({ row }) => ( + <code className="text-xs bg-muted px-2 py-1 rounded"> + {row.getValue("ipAddress")} + </code> + ), + }, + ], + }, + { + id: "추가 정보", + header: "추가 정보", + columns: [ + { + accessorKey: "referrer", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="리퍼러" /> + ), + cell: ({ row }) => { + const referrer = row.getValue("referrer") as string | null + + if (!referrer) { + return <span className="text-muted-foreground text-xs">직접 접근</span> + } + + try { + const url = new URL(referrer) + return ( + <div className="flex items-center gap-1"> + <ExternalLink className="size-3" /> + <span className="text-xs truncate max-w-[100px]"> + {url.hostname} + </span> + </div> + ) + } catch { + return ( + <span className="text-xs truncate max-w-[100px]"> + {referrer} + </span> + ) + } + }, + }, + ], + }, + // { + // id: "actions", + // cell: function Cell({ row }) { + // const visit = row.original + + // 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-40"> + // <DropdownMenuItem + // onSelect={() => setRowAction({ type: "view", row })} + // > + // <Eye className="mr-2 size-4" aria-hidden="true" /> + // 상세 보기 + // </DropdownMenuItem> + // {visit.userEmail && ( + // <DropdownMenuItem + // onSelect={() => setRowAction({ type: "viewUserActivity", row })} + // > + // <User className="mr-2 size-4" aria-hidden="true" /> + // 사용자 활동 + // </DropdownMenuItem> + // )} + // {visit.route && ( + // <DropdownMenuItem + // onSelect={() => setRowAction({ type: "viewPageStats", row })} + // > + // <ExternalLink className="mr-2 size-4" aria-hidden="true" /> + // 페이지 통계 + // </DropdownMenuItem> + // )} + // </DropdownMenuContent> + // </DropdownMenu> + // ) + // }, + // enableSorting: false, + // enableHiding: false, + // }, + ] +}
\ No newline at end of file |
