1
0
forked from baron/baron-sso

dev/admin 테이블 정렬 헤더 UI 공통화

This commit is contained in:
2026-05-13 15:17:37 +09:00
parent 8a8b5baaf6
commit 4a0e5641cb
4 changed files with 102 additions and 106 deletions

View File

@@ -19,7 +19,11 @@ import {
} from "lucide-react";
import * as React from "react";
import { Link, useNavigate } from "react-router-dom";
import { SortableTableHead } from "../../../../../common/core/components/sort";
import {
SortableTableHead,
sortableTableHeadBaseClassName,
sortableTableHeaderClassName,
} from "../../../../../common/core/components/sort";
import {
type SortConfig,
type SortResolverMap,
@@ -954,9 +958,11 @@ function TenantListPage() {
onScroll={handleTenantTableScroll}
>
<Table className="min-w-[1180px]">
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
<TableHeader className={sortableTableHeaderClassName}>
<TableRow>
<TableHead className="w-[48px] whitespace-nowrap">
<TableHead
className={`${sortableTableHeadBaseClassName} w-[48px] whitespace-nowrap`}
>
<Checkbox
checked={
tenants.length > 0 &&
@@ -969,55 +975,57 @@ function TenantListPage() {
/>
</TableHead>
<SortableTableHead
className="min-w-[220px] whitespace-nowrap sticky top-0 bg-inherit"
className="min-w-[220px] whitespace-nowrap"
label={t("ui.admin.tenants.table.id", "ID")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="id"
/>
<SortableTableHead
className="whitespace-nowrap sticky top-0 bg-inherit"
className="whitespace-nowrap"
label={t("ui.admin.tenants.table.name", "NAME")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="name"
/>
<SortableTableHead
className="whitespace-nowrap sticky top-0 bg-inherit"
className="whitespace-nowrap"
label={t("ui.admin.tenants.table.type", "TYPE")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="type"
/>
<SortableTableHead
className="whitespace-nowrap sticky top-0 bg-inherit"
className="whitespace-nowrap"
label={t("ui.admin.tenants.table.slug", "SLUG")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="slug"
/>
<SortableTableHead
className="whitespace-nowrap sticky top-0 bg-inherit"
className="whitespace-nowrap"
label={t("ui.admin.tenants.table.status", "STATUS")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="status"
/>
<SortableTableHead
className="whitespace-nowrap sticky top-0 bg-inherit"
className="whitespace-nowrap"
label={t("ui.admin.tenants.table.members", "MEMBERS")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="recursiveMemberCount"
/>
<SortableTableHead
className="whitespace-nowrap sticky top-0 bg-inherit"
className="whitespace-nowrap"
label={t("ui.admin.tenants.table.created", "CREATED")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="createdAt"
/>
<TableHead className="whitespace-nowrap">
<TableHead
className={`${sortableTableHeadBaseClassName} whitespace-nowrap`}
>
{t("ui.common.actions", "액션")}
</TableHead>
</TableRow>

View File

@@ -1,9 +1,6 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import {
ArrowDown,
ArrowUp,
ArrowUpDown,
ChevronLeft,
ChevronRight,
FileDown,
@@ -22,6 +19,11 @@ import {
sortItems,
toggleSort,
} from "../../../../common/core/utils";
import {
SortableTableHead,
sortableTableHeadBaseClassName,
sortableTableHeaderClassName,
} from "../../../../common/core/components/sort";
import { Button } from "../../components/ui/button";
import {
Card,
@@ -285,17 +287,6 @@ function UserListPage() {
setSortConfig((current) => toggleSort(current, key));
};
const getSortIcon = (key: UserSortKey) => {
if (!sortConfig || sortConfig.key !== key) {
return <ArrowUpDown size={14} className="ml-1 opacity-50" />;
}
return sortConfig.direction === "asc" ? (
<ArrowUp size={14} className="ml-1" />
) : (
<ArrowDown size={14} className="ml-1" />
);
};
const total = query.data?.total ?? 0;
const totalPages = Math.ceil(total / limit);
const canPromoteSuperAdmin = isSuperAdminRole(profile?.role);
@@ -567,9 +558,11 @@ function UserListPage() {
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
<div className="flex-1 overflow-auto relative custom-scrollbar">
<Table>
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
<TableHeader className={sortableTableHeaderClassName}>
<TableRow>
<TableHead className="w-12">
<TableHead
className={`${sortableTableHeadBaseClassName} w-12`}
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
@@ -580,82 +573,61 @@ function UserListPage() {
onChange={toggleSelectAll}
/>
</TableHead>
<TableHead
className="min-w-[220px] whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("id")}
>
<div className="flex items-center">
{t("ui.admin.users.list.table.id", "ID")}
{getSortIcon("id")}
</div>
</TableHead>
<TableHead
className="min-w-[200px] whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("name_email")}
>
<div className="flex items-center">
{t(
"ui.admin.users.list.table.name_email",
"이름 / 이메일 / 전화번호",
)}
{getSortIcon("name_email")}
</div>
</TableHead>
<TableHead
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("status")}
>
<div className="flex items-center">
{t("ui.admin.users.list.table.status", "STATUS")}
{getSortIcon("status")}
</div>
</TableHead>
<TableHead
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("role")}
>
<div className="flex items-center">
{t("ui.admin.users.list.table.role", "ROLE")}
{getSortIcon("role")}
</div>
</TableHead>
<TableHead
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("tenant_dept")}
>
<div className="flex items-center">
{t(
"ui.admin.users.list.table.tenant_dept",
"TENANT / DEPT",
)}
{getSortIcon("tenant_dept")}
</div>
</TableHead>
<SortableTableHead
className="min-w-[220px] whitespace-nowrap"
label={t("ui.admin.users.list.table.id", "ID")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="id"
/>
<SortableTableHead
className="min-w-[200px] whitespace-nowrap"
label={t(
"ui.admin.users.list.table.name_email",
"이름 / 이메일 / 전화번호",
)}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="name_email"
/>
<SortableTableHead
className="whitespace-nowrap"
label={t("ui.admin.users.list.table.status", "STATUS")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="status"
/>
<SortableTableHead
className="whitespace-nowrap"
label={t(
"ui.admin.users.list.table.tenant_dept",
"TENANT / DEPT",
)}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="tenant_dept"
/>
{/* Dynamic Columns from Schema */}
{userSchema.map(
(field) =>
visibleColumns[field.key] !== false && (
<TableHead
<SortableTableHead
key={field.key}
className="whitespace-nowrap uppercase cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort(field.key)}
>
<div className="flex items-center">
{field.label}
{getSortIcon(field.key)}
</div>
</TableHead>
className="whitespace-nowrap"
label={field.label}
onSort={requestSort}
sortConfig={sortConfig}
sortKey={field.key}
/>
),
)}
<TableHead
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("createdAt")}
>
<div className="flex items-center">
{t("ui.admin.users.list.table.created", "CREATED")}
{getSortIcon("createdAt")}
</div>
</TableHead>
<SortableTableHead
className="whitespace-nowrap"
label={t("ui.admin.users.list.table.created", "CREATED")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="createdAt"
/>
</TableRow>
</TableHeader>
<TableBody>

View File

@@ -1,12 +1,18 @@
import type { ReactNode, ThHTMLAttributes } from "react";
import type { SortConfig } from "../../utils";
export const sortableTableHeadBaseClassName =
"h-12 px-6 text-left text-xs font-sans font-bold uppercase tracking-[0.08em] text-foreground align-middle";
export const sortableTableHeaderClassName =
"sticky top-0 z-10 bg-secondary shadow-sm";
function SortAscendingIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4"
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeWidth="2"
@@ -25,7 +31,7 @@ function SortDescendingIcon() {
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4"
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeWidth="2"
@@ -44,7 +50,7 @@ function SortIdleIcon() {
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4 opacity-50"
className="ml-1 h-3.5 w-3.5 opacity-50"
fill="none"
stroke="currentColor"
strokeWidth="2"
@@ -122,7 +128,7 @@ export function SortableTableHead<Key extends string>({
<th
aria-sort={sortAriaValue(isActive, direction)}
className={[
"h-12 px-6 text-xs font-bold uppercase tracking-[0.08em] align-middle",
sortableTableHeadBaseClassName,
alignClassName(align),
disabled ? "" : "transition-colors hover:bg-muted/50",
className,
@@ -136,7 +142,7 @@ export function SortableTableHead<Key extends string>({
onClick={() => onSort(sortKey)}
disabled={disabled}
className={[
"flex w-full items-center gap-1",
"flex w-full items-center font-inherit",
buttonAlignClassName(align),
disabled ? "cursor-default opacity-70" : "cursor-pointer",
contentClassName,
@@ -146,9 +152,13 @@ export function SortableTableHead<Key extends string>({
>
<span>{label}</span>
{direction === "asc" ? (
<SortAscendingIcon />
<span className="ml-1 inline-flex">
<SortAscendingIcon />
</span>
) : direction === "desc" ? (
<SortDescendingIcon />
<span className="ml-1 inline-flex">
<SortDescendingIcon />
</span>
) : (
<SortIdleIcon />
)}

View File

@@ -10,7 +10,11 @@ import {
import { useEffect, useMemo, useState } from "react";
import { useAuth } from "react-oidc-context";
import { Link, useNavigate } from "react-router-dom";
import { SortableTableHead } from "../../../../common/core/components/sort";
import {
SortableTableHead,
sortableTableHeadBaseClassName,
sortableTableHeaderClassName,
} from "../../../../common/core/components/sort";
import {
type SortConfig,
type SortResolverMap,
@@ -440,7 +444,7 @@ function ClientsPage() {
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableHeader className={sortableTableHeaderClassName}>
<TableRow>
<SortableTableHead
label={t("ui.dev.clients.table.application", "애플리케이션")}
@@ -472,7 +476,9 @@ function ClientsPage() {
sortConfig={sortConfig}
sortKey="createdAt"
/>
<TableHead className="text-right">
<TableHead
className={`${sortableTableHeadBaseClassName} text-right`}
>
{t("ui.dev.clients.table.actions", "액션")}
</TableHead>
</TableRow>