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

View File

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

View File

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

View File

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