diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx
index 8739e474..b2622121 100644
--- a/adminfront/src/features/tenants/routes/TenantListPage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx
@@ -2,9 +2,6 @@ import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import type { AxiosError } from "axios";
import {
- ArrowDown,
- ArrowUp,
- ArrowUpDown,
Building2,
ChevronDown,
ChevronRight,
@@ -22,6 +19,7 @@ import {
} from "lucide-react";
import * as React from "react";
import { Link, useNavigate } from "react-router-dom";
+import { SortableTableHead } from "../../../../../common/core/components/sort";
import {
type SortConfig,
type SortResolverMap,
@@ -513,17 +511,6 @@ function TenantListPage() {
setSortConfig((current) => toggleSort(current, key));
};
- const getSortIcon = (key: TenantSortKey) => {
- if (!sortConfig || sortConfig.key !== key) {
- return ;
- }
- return sortConfig.direction === "asc" ? (
-
- ) : (
-
- );
- };
-
const deletableTenants = React.useMemo(
() => tenants.filter((tenant) => !isSeedTenant(tenant)),
[tenants],
@@ -977,69 +964,55 @@ function TenantListPage() {
}
/>
- requestSort("id")}
- >
-
- {t("ui.admin.tenants.table.id", "ID")}
- {getSortIcon("id")}
-
-
- requestSort("name")}
- >
-
- {t("ui.admin.tenants.table.name", "NAME")}
- {getSortIcon("name")}
-
-
- requestSort("type")}
- >
-
- {t("ui.admin.tenants.table.type", "TYPE")}
- {getSortIcon("type")}
-
-
- requestSort("slug")}
- >
-
- {t("ui.admin.tenants.table.slug", "SLUG")}
- {getSortIcon("slug")}
-
-
- requestSort("status")}
- >
-
- {t("ui.admin.tenants.table.status", "STATUS")}
- {getSortIcon("status")}
-
-
- requestSort("recursiveMemberCount")}
- >
-
- {t("ui.admin.tenants.table.members", "MEMBERS")}
- {getSortIcon("recursiveMemberCount")}
-
-
- requestSort("updatedAt")}
- >
-
- {t("ui.admin.tenants.table.updated", "UPDATED")}
- {getSortIcon("updatedAt")}
-
-
+
+
+
+
+
+
+
{t("ui.common.actions", "액션")}
diff --git a/adminfront/tsconfig.app.json b/adminfront/tsconfig.app.json
index 91425f60..b9a4130a 100644
--- a/adminfront/tsconfig.app.json
+++ b/adminfront/tsconfig.app.json
@@ -15,6 +15,13 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
+ "baseUrl": ".",
+ "paths": {
+ "lucide-react": ["./node_modules/lucide-react"],
+ "react": ["./node_modules/@types/react/index.d.ts"],
+ "react/jsx-dev-runtime": ["./node_modules/@types/react/jsx-dev-runtime.d.ts"],
+ "react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime.d.ts"]
+ },
/* Linting */
"strict": true,
@@ -24,6 +31,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
- "include": ["src"],
+ "include": ["src", "../common/**/*.ts", "../common/**/*.tsx"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
}
diff --git a/adminfront/vite.config.ts b/adminfront/vite.config.ts
index ee24ae9f..3307996f 100644
--- a/adminfront/vite.config.ts
+++ b/adminfront/vite.config.ts
@@ -1,4 +1,5 @@
import react from "@vitejs/plugin-react";
+import path from "node:path";
import { defineConfig } from "vite";
const buildOutDir =
@@ -6,6 +7,20 @@ const buildOutDir =
export default defineConfig({
plugins: [react()],
+ resolve: {
+ alias: {
+ "lucide-react": path.resolve(__dirname, "node_modules/lucide-react"),
+ react: path.resolve(__dirname, "node_modules/react"),
+ "react/jsx-dev-runtime": path.resolve(
+ __dirname,
+ "node_modules/react/jsx-dev-runtime.js",
+ ),
+ "react/jsx-runtime": path.resolve(
+ __dirname,
+ "node_modules/react/jsx-runtime.js",
+ ),
+ },
+ },
envPrefix: ["VITE_", "USERFRONT_", "ORGFRONT_"],
cacheDir:
process.env.ADMINFRONT_VITE_CACHE_DIR ??
diff --git a/common/core/components/sort/SortableTableHead.tsx b/common/core/components/sort/SortableTableHead.tsx
new file mode 100644
index 00000000..b6774a85
--- /dev/null
+++ b/common/core/components/sort/SortableTableHead.tsx
@@ -0,0 +1,158 @@
+import type { ReactNode, ThHTMLAttributes } from "react";
+import type { SortConfig } from "../../utils";
+
+function SortAscendingIcon() {
+ return (
+
+ );
+}
+
+function SortDescendingIcon() {
+ return (
+
+ );
+}
+
+function SortIdleIcon() {
+ return (
+
+ );
+}
+
+type SortableTableHeadAlign = "left" | "center" | "right";
+
+function alignClassName(align: SortableTableHeadAlign) {
+ switch (align) {
+ case "center":
+ return "text-center";
+ case "right":
+ return "text-right";
+ default:
+ return "text-left";
+ }
+}
+
+function buttonAlignClassName(align: SortableTableHeadAlign) {
+ switch (align) {
+ case "center":
+ return "justify-center";
+ case "right":
+ return "justify-end";
+ default:
+ return "justify-start";
+ }
+}
+
+function sortAriaValue(
+ isActive: boolean,
+ direction: "asc" | "desc" | null,
+): ThHTMLAttributes["aria-sort"] {
+ if (!isActive || direction === null) {
+ return "none";
+ }
+ return direction === "asc" ? "ascending" : "descending";
+}
+
+type SortableTableHeadProps = Omit<
+ ThHTMLAttributes,
+ "children"
+> & {
+ align?: SortableTableHeadAlign;
+ contentClassName?: string;
+ disabled?: boolean;
+ label: ReactNode;
+ onSort: (key: Key) => void;
+ sortConfig: SortConfig | null;
+ sortKey: Key;
+};
+
+export function SortableTableHead({
+ align = "left",
+ className = "",
+ contentClassName = "",
+ disabled = false,
+ label,
+ onSort,
+ sortConfig,
+ sortKey,
+ ...props
+}: SortableTableHeadProps) {
+ const isActive = sortConfig?.key === sortKey;
+ const direction = isActive ? sortConfig?.direction ?? null : null;
+
+ return (
+
+
+ |
+ );
+}
diff --git a/common/core/components/sort/index.ts b/common/core/components/sort/index.ts
new file mode 100644
index 00000000..a9d87fb9
--- /dev/null
+++ b/common/core/components/sort/index.ts
@@ -0,0 +1 @@
+export * from "./SortableTableHead";
diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx
index 866a7a1c..fcc26b1d 100644
--- a/devfront/src/features/clients/ClientsPage.tsx
+++ b/devfront/src/features/clients/ClientsPage.tsx
@@ -1,9 +1,6 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import {
- ArrowDown,
- ArrowUp,
- ArrowUpDown,
BookOpenText,
Filter,
Plus,
@@ -13,6 +10,7 @@ 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 {
type SortConfig,
type SortResolverMap,
@@ -230,18 +228,6 @@ function ClientsPage() {
setSortConfig((current) => toggleSort(current, key));
};
- const getSortIcon = (key: ClientSortKey) => {
- if (!sortConfig || sortConfig.key !== key) {
- return ;
- }
-
- return sortConfig.direction === "asc" ? (
-
- ) : (
-
- );
- };
-
if (auth.isLoading || !hasAccessToken || isLoading) {
return (
@@ -452,51 +438,41 @@ function ClientsPage() {
- requestSort("application")}
- >
-
- {t("ui.dev.clients.table.application", "애플리케이션")}
- {getSortIcon("application")}
-
-
- requestSort("id")}
- >
-
- {t("ui.dev.clients.table.client_id", "Client ID")}
- {getSortIcon("id")}
-
-
- requestSort("type")}
- >
-
- {t("ui.dev.clients.table.type", "유형")}
- {getSortIcon("type")}
-
-
- requestSort("status")}
- >
-
- {t("ui.dev.clients.table.status", "상태")}
- {getSortIcon("status")}
-
-
- requestSort("createdAt")}
- >
-
- {t("ui.dev.clients.table.created_at", "생성일")}
- {getSortIcon("createdAt")}
-
-
+
+
+
+
+
{t("ui.dev.clients.table.actions", "액션")}
diff --git a/devfront/tsconfig.app.json b/devfront/tsconfig.app.json
index 68025a7f..5e4fc7d2 100644
--- a/devfront/tsconfig.app.json
+++ b/devfront/tsconfig.app.json
@@ -15,6 +15,13 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
+ "baseUrl": ".",
+ "paths": {
+ "lucide-react": ["./node_modules/lucide-react"],
+ "react": ["./node_modules/@types/react/index.d.ts"],
+ "react/jsx-dev-runtime": ["./node_modules/@types/react/jsx-dev-runtime.d.ts"],
+ "react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime.d.ts"]
+ },
/* Linting */
"strict": true,
@@ -24,6 +31,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
- "include": ["src"],
+ "include": ["src", "../common/**/*.ts", "../common/**/*.tsx"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
}
diff --git a/devfront/vite.config.ts b/devfront/vite.config.ts
index 1d03cb5d..01db8ec8 100644
--- a/devfront/vite.config.ts
+++ b/devfront/vite.config.ts
@@ -1,4 +1,5 @@
import react from "@vitejs/plugin-react";
+import path from "node:path";
import { defineConfig } from "vite";
const buildOutDir =
@@ -35,6 +36,20 @@ const allowedHosts = Array.from(
export default defineConfig({
plugins: [react()],
+ resolve: {
+ alias: {
+ "lucide-react": path.resolve(__dirname, "node_modules/lucide-react"),
+ react: path.resolve(__dirname, "node_modules/react"),
+ "react/jsx-dev-runtime": path.resolve(
+ __dirname,
+ "node_modules/react/jsx-dev-runtime.js",
+ ),
+ "react/jsx-runtime": path.resolve(
+ __dirname,
+ "node_modules/react/jsx-runtime.js",
+ ),
+ },
+ },
cacheDir:
process.env.DEVFRONT_VITE_CACHE_DIR ?? "/tmp/baron-sso-devfront-vite-cache",
build: {