diff options
Diffstat (limited to 'lib/login-session/table')
3 files changed, 458 insertions, 0 deletions
diff --git a/lib/login-session/table/login-sessions-table-columns.tsx b/lib/login-session/table/login-sessions-table-columns.tsx new file mode 100644 index 00000000..e3d8bc2f --- /dev/null +++ b/lib/login-session/table/login-sessions-table-columns.tsx @@ -0,0 +1,243 @@ +"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, + DropdownMenuShortcut, + 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 { ExtendedLoginSession } from "../validation" +import { Eye, Shield, LogOut, Ellipsis } from "lucide-react" + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ExtendedLoginSession> | null>> +} + +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ExtendedLoginSession>[] { + 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 }) => ( + <div className="flex flex-col"> + <span className="font-medium">{row.getValue("userEmail")}</span> + <span className="text-xs text-muted-foreground"> + {row.original.userName} + </span> + </div> + ), + }, + ], + }, + { + id: "세션 정보", + header: "세션 정보", + columns: [ + { + accessorKey: "loginAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="로그인 시간" /> + ), + cell: ({ row }) => { + const date = row.getValue("loginAt") as Date + return ( + <Tooltip> + <TooltipTrigger> + <div className="text-sm"> + {formatDate(date, "KR")} + </div> + </TooltipTrigger> + <TooltipContent> + {formatDate(date)} + </TooltipContent> + </Tooltip> + ) + }, + }, + { + accessorKey: "logoutAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="로그아웃 시간" /> + ), + cell: ({ row }) => { + const date = row.getValue("logoutAt") as Date | null + if (!date) { + return <span className="text-muted-foreground">-</span> + } + return ( + <Tooltip> + <TooltipTrigger> + <div className="text-sm"> + {formatDate(date, "KR")} + </div> + </TooltipTrigger> + <TooltipContent> + {formatDate(date)} + </TooltipContent> + </Tooltip> + ) + }, + }, + { + accessorKey: "sessionDuration", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="세션 지속시간" /> + ), + cell: ({ row }) => { + const duration = row.getValue("sessionDuration") as number | null + if (!duration) { + return <span className="text-muted-foreground">-</span> + } + + const hours = Math.floor(duration / 60) + const minutes = Math.floor(duration % 60) + + if (hours > 0) { + return `${hours}시간 ${minutes}분` + } + return `${minutes}분` + }, + }, + ], + }, + { + id: "인증 및 보안", + header: "인증 및 보안", + columns: [ + { + accessorKey: "authMethod", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="인증 방식" /> + ), + cell: ({ row }) => { + const authMethod = row.getValue("authMethod") as string + const variants = { + otp: "default", + email: "secondary", + sgips: "outline", + saml: "destructive", + } as const + + return ( + <Badge variant={variants[authMethod as keyof typeof variants] || "default"}> + {authMethod.toUpperCase()} + </Badge> + ) + }, + }, + { + 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> + ), + }, + { + accessorKey: "isCurrentlyActive", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="상태" /> + ), + cell: ({ row }) => { + const isActive = row.getValue("isCurrentlyActive") as boolean + return ( + <Badge variant={isActive ? "default" : "secondary"}> + {isActive ? "활성" : "비활성"} + </Badge> + ) + }, + }, + ], + }, + { + id: "actions", + cell: function Cell({ row }) { + const session = 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> + <DropdownMenuItem + onSelect={() => setRowAction({ type: "viewSecurity", row })} + > + <Shield className="mr-2 size-4" aria-hidden="true" /> + 보안 정보 + </DropdownMenuItem> + {session.isCurrentlyActive && ( + <DropdownMenuItem + onSelect={() => setRowAction({ type: "forceLogout", row })} + className="text-red-600" + > + <LogOut className="mr-2 size-4" aria-hidden="true" /> + 강제 로그아웃 + <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + ) + }, + enableSorting: false, + enableHiding: false, + }, + ] +}
\ No newline at end of file diff --git a/lib/login-session/table/login-sessions-table-toolbar-actions.tsx b/lib/login-session/table/login-sessions-table-toolbar-actions.tsx new file mode 100644 index 00000000..36665bc0 --- /dev/null +++ b/lib/login-session/table/login-sessions-table-toolbar-actions.tsx @@ -0,0 +1,78 @@ +"use client" + +import { type Table } from "@tanstack/react-table" +import { Download, RotateCcw, Shield } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" + +import { ExtendedLoginSession } from "../validation" +import { exportTableToExcel } from "@/lib/export_all" + +interface LoginSessionsTableToolbarActionsProps { + table: Table<ExtendedLoginSession> +} + +export function LoginSessionsTableToolbarActions({ + table, +}: LoginSessionsTableToolbarActionsProps) { + return ( + <div className="flex items-center gap-2"> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => + exportTableToExcel(table, { + filename: "login-sessions", + excludeColumns: ["select", "actions"], + }) + } + > + <Download className="mr-2 size-4" aria-hidden="true" /> + Export + </Button> + </TooltipTrigger> + <TooltipContent> + <p>로그인 세션 데이터를 엑셀로 내보내기</p> + </TooltipContent> + </Tooltip> + + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => window.location.reload()} + > + <RotateCcw className="mr-2 size-4" aria-hidden="true" /> + 새로고침 + </Button> + </TooltipTrigger> + <TooltipContent> + <p>데이터 새로고침</p> + </TooltipContent> + </Tooltip> + + <Tooltip> + <TooltipTrigger asChild> + <Button + variant="outline" + size="sm" + onClick={() => { + // 보안 리포트 생성 기능 + console.log("Generate security report") + }} + > + <Shield className="mr-2 size-4" aria-hidden="true" /> + 보안 리포트 + </Button> + </TooltipTrigger> + <TooltipContent> + <p>보안 분석 리포트 생성</p> + </TooltipContent> + </Tooltip> + </div> + ) +}
\ No newline at end of file diff --git a/lib/login-session/table/login-sessions-table.tsx b/lib/login-session/table/login-sessions-table.tsx new file mode 100644 index 00000000..43568f41 --- /dev/null +++ b/lib/login-session/table/login-sessions-table.tsx @@ -0,0 +1,137 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { useFeatureFlags } from "@/components/data-table/feature-flags-provider" + +import { getLoginSessions } from "../service" +import { LoginSessionsTableToolbarActions } from "./login-sessions-table-toolbar-actions" +import { getColumns } from "./login-sessions-table-columns" +import { ExtendedLoginSession } from "../validation" + +interface LoginSessionsTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getLoginSessions>>, + ] + > +} + +export function LoginSessionsTable({ promises }: LoginSessionsTableProps) { + const { featureFlags } = useFeatureFlags() + + const [{ data, pageCount }] = React.use(promises) + + const [rowAction, setRowAction] = + React.useState<DataTableRowAction<ExtendedLoginSession> | null>(null) + + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + // 기본 필터 필드 + const filterFields: DataTableFilterField<ExtendedLoginSession>[] = [ + { + id: "authMethod", + label: "인증 방식", + options: [ + { label: "OTP", value: "otp" }, + { label: "Email", value: "email" }, + { label: "SGIPS", value: "sgips" }, + { label: "SAML", value: "saml" }, + ], + }, + { + id: "isActive", + label: "세션 상태", + options: [ + { label: "활성", value: "true" }, + { label: "비활성", value: "false" }, + ], + }, + ] + + // 고급 필터 필드 + const advancedFilterFields: DataTableAdvancedFilterField<ExtendedLoginSession>[] = [ + { + id: "userEmail", + label: "사용자 이메일", + type: "text", + }, + { + id: "userName", + label: "사용자 이름", + type: "text", + }, + { + id: "authMethod", + label: "인증 방식", + type: "multi-select", + options: [ + { label: "OTP", value: "otp" }, + { label: "Email", value: "email" }, + { label: "SGIPS", value: "sgips" }, + { label: "SAML", value: "saml" }, + ], + }, + { + id: "ipAddress", + label: "IP 주소", + type: "text", + }, + { + id: "isActive", + label: "활성 상태", + type: "boolean", + }, + { + id: "loginAt", + label: "로그인 시간", + type: "date", + }, + { + id: "logoutAt", + label: "로그아웃 시간", + type: "date", + }, + ] + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "loginAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + <DataTable table={table}> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <LoginSessionsTableToolbarActions table={table} /> + </DataTableAdvancedToolbar> + </DataTable> + </> + ) +}
\ No newline at end of file |
