1
0
forked from baron/baron-sso

테넌트 목록 및 조직 계층 구조 개선

This commit is contained in:
2026-02-27 10:29:15 +09:00
parent 600961f33d
commit ca45a14bae
27 changed files with 1906 additions and 806 deletions

View File

@@ -29,8 +29,8 @@ function TenantCreatePage() {
const [domains, setDomains] = useState("");
const parentQuery = useQuery({
queryKey: ["tenants", { limit: 100 }],
queryFn: () => fetchTenants(100, 0),
queryKey: ["tenants", { limit: 1000 }],
queryFn: () => fetchTenants(1000, 0),
});
const mutation = useMutation({

View File

@@ -27,118 +27,13 @@ import {
} from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";
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 (!node) continue;
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: 1000, offset: 0 }], // Fetch all to build tree
queryKey: ["tenants", { limit: 1000, offset: 0 }],
queryFn: () => fetchTenants(1000, 0),
});
const navigate = useNavigate();
const deleteMutation = useMutation({
mutationFn: (tenantId: string) => deleteTenant(tenantId),
onSuccess: () => {
@@ -153,7 +48,7 @@ function TenantListPage() {
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
: null;
const tenantTree = query.data?.items ? buildTenantTree(query.data.items) : [];
const tenants = query.data?.items ?? [];
const handleDelete = (tenantId: string, tenantName: string) => {
if (
@@ -182,12 +77,12 @@ function TenantListPage() {
</span>
</div>
<h2 className="text-3xl font-semibold">
{t("ui.admin.tenants.title", "테넌트 목록")}
{t("ui.admin.tenants.title", "테넌트 레지스트리")}
</h2>
<p className="text-sm text-[var(--color-muted)]">
{t(
"msg.admin.tenants.subtitle",
"현재 등록된 테넌트를 확인하고 상태를 관리합니다.",
"시스템에 등록된 모든 테넌트를 평면 목록으로 확인하고 관리합니다.",
)}
</p>
</div>
@@ -213,7 +108,7 @@ function TenantListPage() {
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>
{t("ui.admin.tenants.registry.title", "Tenant registry")}
{t("ui.admin.tenants.registry.title", "Tenant Registry")}
</CardTitle>
<CardDescription>
{t("msg.admin.tenants.registry.count", "총 {{count}}개 테넌트", {
@@ -247,6 +142,9 @@ function TenantListPage() {
<TableHead>
{t("ui.admin.tenants.table.status", "STATUS")}
</TableHead>
<TableHead>
{t("ui.admin.tenants.table.members", "MEMBERS")}
</TableHead>
<TableHead>
{t("ui.admin.tenants.table.updated", "UPDATED")}
</TableHead>
@@ -258,15 +156,15 @@ function TenantListPage() {
<TableBody>
{query.isLoading && (
<TableRow>
<TableCell colSpan={6}>
<TableCell colSpan={7}>
{t("msg.common.loading", "로딩 중...")}
</TableCell>
</TableRow>
)}
{!query.isLoading && tenantTree.length === 0 && (
{!query.isLoading && tenants.length === 0 && (
<TableRow>
<TableCell
colSpan={6}
colSpan={7}
className="text-center py-8 text-muted-foreground"
>
{t(
@@ -276,14 +174,63 @@ function TenantListPage() {
</TableCell>
</TableRow>
)}
{tenantTree.map((tenant) => (
<TenantRow
key={tenant.id}
tenant={tenant}
level={0}
onDelete={handleDelete}
isDeleting={deleteMutation.isPending}
/>
{tenants.map((tenant) => (
<TableRow key={tenant.id}>
<TableCell className="font-semibold">{tenant.name}</TableCell>
<TableCell>
<Badge variant="outline" className="text-[10px] font-mono">
{t(
`domain.tenant_type.${tenant.type?.toLowerCase()}`,
tenant.type,
)}
</Badge>
</TableCell>
<TableCell className="font-mono text-xs">
{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 className="font-medium">
{tenant.memberCount}
</TableCell>
<TableCell className="text-xs">
{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>
))}
</TableBody>
</Table>