1
0
forked from baron/baron-sso

유저 그룹 계층형 보기

This commit is contained in:
2026-02-23 15:53:18 +09:00
parent 1c6fb4ef83
commit 73b0453ad4
9 changed files with 705 additions and 394 deletions

View File

@@ -1,6 +1,13 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
import {
CornerDownRight,
Pencil,
Plus,
RefreshCw,
Trash2,
} from "lucide-react";
import React from "react";
import { Link, useNavigate } from "react-router-dom";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
@@ -19,14 +26,119 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import { deleteTenant, fetchTenants } from "../../../lib/adminApi";
import {
deleteTenant,
fetchTenants,
type TenantSummary,
} from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";
function TenantListPage() {
type TenantNode = TenantSummary & { children: TenantNode[] };
function buildTenantTree(tenants: TenantSummary[]): TenantNode[] {
const tenantMap = new Map<string, TenantNode>();
const rootTenants: TenantNode[] = [];
for (const tenant of tenants) {
tenantMap.set(tenant.id, { ...tenant, children: [] });
}
for (const tenant of tenants) {
const node = tenantMap.get(tenant.id)!;
if (tenant.parentId) {
const parent = tenantMap.get(tenant.parentId);
if (parent) {
parent.children.push(node);
} else {
rootTenants.push(node); // Orphaned
}
} else {
rootTenants.push(node);
}
}
return rootTenants;
}
const TenantRow: React.FC<{
tenant: TenantNode;
level: number;
onDelete: (id: string, name: string) => void;
isDeleting: boolean;
}> = ({ tenant, level, onDelete, isDeleting }) => {
const navigate = useNavigate();
return (
<>
<TableRow key={tenant.id}>
<TableCell style={{ paddingLeft: `${1 + level * 1.5}rem` }}>
<div className="flex items-center gap-2">
{level > 0 && <CornerDownRight size={14} className="text-muted-foreground" />}
<span className="font-semibold">{tenant.name}</span>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="text-[10px] font-mono">
{tenant.type || "PERSONAL"}
</Badge>
</TableCell>
<TableCell>{tenant.slug}</TableCell>
<TableCell>
<Badge
variant={
tenant.status === "active"
? "default"
: tenant.status === "pending"
? "secondary"
: "muted"
}
>
{t(`ui.common.status.${tenant.status}`, tenant.status)}
</Badge>
</TableCell>
<TableCell>
{tenant.updatedAt
? new Date(tenant.updatedAt).toLocaleString("ko-KR")
: "-"}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/tenants/${tenant.id}`)}
>
<Pencil size={14} />
{t("ui.common.edit", "편집")}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onDelete(tenant.id, tenant.name)}
disabled={isDeleting}
>
<Trash2 size={14} />
{t("ui.common.delete", "삭제")}
</Button>
</div>
</TableCell>
</TableRow>
{tenant.children.map((child) => (
<TenantRow
key={child.id}
tenant={child}
level={level + 1}
onDelete={onDelete}
isDeleting={isDeleting}
/>
))}
</>
);
};
function TenantListPage() {
const query = useQuery({
queryKey: ["tenants", { limit: 50, offset: 0 }],
queryFn: () => fetchTenants(50, 0),
queryKey: ["tenants", { limit: 1000, offset: 0 }], // Fetch all to build tree
queryFn: () => fetchTenants(1000, 0),
});
const deleteMutation = useMutation({
@@ -43,7 +155,7 @@ function TenantListPage() {
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
: null;
const items = query.data?.items ?? [];
const tenantTree = query.data?.items ? buildTenantTree(query.data.items) : [];
const handleDelete = (tenantId: string, tenantName: string) => {
if (
@@ -148,14 +260,14 @@ function TenantListPage() {
<TableBody>
{query.isLoading && (
<TableRow>
<TableCell colSpan={5}>
<TableCell colSpan={6}>
{t("msg.common.loading", "로딩 중...")}
</TableCell>
</TableRow>
)}
{!query.isLoading && items.length === 0 && (
{!query.isLoading && tenantTree.length === 0 && (
<TableRow>
<TableCell colSpan={5}>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
{t(
"msg.admin.tenants.empty",
"아직 등록된 테넌트가 없습니다.",
@@ -163,60 +275,14 @@ function TenantListPage() {
</TableCell>
</TableRow>
)}
{items.map((tenant) => (
<TableRow key={tenant.id}>
<TableCell className="font-semibold">{tenant.name}</TableCell>
<TableCell>
<Badge variant="outline" className="text-[10px] font-mono">
{tenant.type || "PERSONAL"}
</Badge>
</TableCell>
<TableCell>{tenant.slug}</TableCell>
<TableCell>
<Badge
variant={
tenant.status === "active"
? "default"
: tenant.status === "pending"
? "secondary"
: "muted"
}
className={
tenant.status === "pending"
? "bg-yellow-100 text-yellow-800"
: ""
}
>
{t(`ui.common.status.${tenant.status}`, tenant.status)}
</Badge>
</TableCell>
<TableCell>
{tenant.updatedAt
? new Date(tenant.updatedAt).toLocaleString("ko-KR")
: "-"}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/tenants/${tenant.id}`)}
>
<Pencil size={14} />
{t("ui.common.edit", "편집")}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDelete(tenant.id, tenant.name)}
disabled={deleteMutation.isPending}
>
<Trash2 size={14} />
{t("ui.common.delete", "삭제")}
</Button>
</div>
</TableCell>
</TableRow>
{tenantTree.map((tenant) => (
<TenantRow
key={tenant.id}
tenant={tenant}
level={0}
onDelete={handleDelete}
isDeleting={deleteMutation.isPending}
/>
))}
</TableBody>
</Table>
@@ -227,3 +293,4 @@ function TenantListPage() {
}
export default TenantListPage;