1
0
forked from baron/baron-sso

테넌트 레지스트리 테이블 UI 복원

This commit is contained in:
2026-06-05 15:32:00 +09:00
parent b4883bc9eb
commit 729a9890a6
4 changed files with 85 additions and 99 deletions

View File

@@ -1,9 +1,4 @@
import {
type UseMutationResult,
useInfiniteQuery,
useMutation,
useQuery,
} from "@tanstack/react-query";
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import type { AxiosError } from "axios";
import {
@@ -41,7 +36,6 @@ import { Button } from "../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
@@ -69,7 +63,6 @@ import {
SelectTrigger,
SelectValue,
} from "../../../components/ui/select";
import { Switch } from "../../../components/ui/switch";
import {
Table,
TableBody,
@@ -89,7 +82,6 @@ import {
type TenantImportDetail,
type TenantImportResult,
type TenantSummary,
type UserProfileResponse,
updateTenant,
} from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";
@@ -416,27 +408,6 @@ function TenantListPage() {
},
});
const statusMutation = useMutation({
mutationFn: ({
tenantId,
status,
}: {
tenantId: string;
status: string;
}) => updateTenant(tenantId, { status }),
onSuccess: () => {
query.refetch();
toast.success(
t("msg.admin.tenants.bulk.update_success", "선택한 테넌트들의 상태가 수정되었습니다."),
);
},
onError: () => {
toast.error(
t("msg.admin.tenants.bulk.update_error", "테넌트 일괄 상태 변경에 실패했습니다."),
);
},
});
const handleApplyBulkStatus = () => {
if (selectedIds.length === 0 || !selectedBulkStatus) return;
bulkUpdateStatusMutation.mutate({
@@ -504,7 +475,6 @@ function TenantListPage() {
? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.")
: null;
const tenantTotal = query.data?.pages[0]?.total ?? 0;
const hanmacFamilyTenantId = React.useMemo(() => {
const envTenantId = import.meta.env.VITE_HANMAC_FAMILY_TENANT_ID;
if (typeof envTenantId === "string" && envTenantId.trim()) {
@@ -962,15 +932,6 @@ function TenantListPage() {
<CardTitle className="text-lg font-bold flex items-center gap-2">
{t("ui.admin.tenants.registry.title", "Tenant Registry")}
</CardTitle>
<CardDescription>
{t(
"msg.admin.tenants.registry.count",
"총 {{count}}개의 테넌트가 등록되어 있습니다.",
{
count: scopeTenantId ? scopedTenants.length : tenantTotal,
},
)}
</CardDescription>
</div>
</div>
</CardHeader>
@@ -988,8 +949,6 @@ function TenantListPage() {
onSelectAll={handleSelectAll}
search={search}
deletableTenants={deletableTenants}
statusMutation={statusMutation}
profile={profile}
sortConfig={sortConfig}
requestSort={requestSort}
getSortIcon={getSortIcon}
@@ -1566,13 +1525,6 @@ const TenantHierarchyView: React.FC<{
onSelectAll: (checked: boolean) => void;
search: string;
deletableTenants: TenantSummary[];
statusMutation: UseMutationResult<
TenantSummary,
Error,
{ tenantId: string; status: string },
unknown
>;
profile: UserProfileResponse | undefined;
sortConfig: SortConfig<TenantSortKey> | null;
requestSort: (key: TenantSortKey) => void;
getSortIcon: (key: TenantSortKey) => React.ReactNode;
@@ -1589,8 +1541,6 @@ const TenantHierarchyView: React.FC<{
onSelectAll,
search,
deletableTenants,
statusMutation,
profile,
sortConfig,
requestSort,
getSortIcon,
@@ -1829,69 +1779,99 @@ const TenantHierarchyView: React.FC<{
className="mr-2 flex-shrink-0 text-muted-foreground"
/>
<div className="flex min-w-0 flex-wrap items-center gap-2">
<Link
to={`/tenants/${node.id}`}
className="cursor-pointer truncate text-primary hover:underline"
>
{node.name}
</Link>
{isSeedTenant(node) && (
<Badge
variant="secondary"
className="flex-shrink-0 text-[10px]"
<div className="min-w-0">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<Link
to={`/tenants/${node.id}`}
className="block max-w-full truncate text-foreground transition-colors hover:text-primary hover:underline"
>
{t("ui.admin.tenants.seed_badge", "초기 설정")}
</Badge>
)}
{node.name}
</Link>
{isSeedTenant(node) && (
<Badge
variant="secondary"
className="flex-shrink-0 text-[10px]"
>
{t("ui.admin.tenants.seed_badge", "초기 설정")}
</Badge>
)}
</div>
{(() => {
const parentPath = tenantParentPathMap.get(node.id) ?? [];
return (
<p className="mt-0.5 truncate text-xs font-normal text-muted-foreground">
{parentPath.length > 0
? parentPath.join(" / ")
: t("ui.admin.tenants.path.root", "최상위")}
</p>
);
})()}
</div>
</div>
</TableCell>
<TableCell
className="max-w-[260px] break-all font-mono text-xs text-muted-foreground"
className="whitespace-nowrap"
data-testid={`tenant-internal-id-${node.id}`}
>
{node.id}
<code className="inline-block rounded-md bg-secondary/60 px-2 py-1 font-mono text-xs text-muted-foreground">
{node.id}
</code>
</TableCell>
<TableCell className="whitespace-nowrap">
<Badge variant="outline" className="font-mono text-[10px]">
{node.type}
<span
className={cn(
"text-xs font-medium uppercase tracking-[0.04em]",
getTenantTypeTextClass(node.type),
)}
>
{getTenantTypeLabel(node.type)}
</span>
</TableCell>
<TableCell className="whitespace-nowrap">
<code className="inline-flex max-w-full items-center rounded-md bg-secondary/60 px-2 py-1 font-mono text-xs text-muted-foreground">
{node.slug}
</code>
</TableCell>
<TableCell className="whitespace-nowrap">
<Badge
variant={node.status === "active" ? "default" : "muted"}
className={cn(
"px-3 py-1 text-xs uppercase",
node.status === "active"
? "border-transparent bg-blue-500 text-white hover:bg-blue-500/90 hover:text-white"
: "border-border bg-secondary/60 text-muted-foreground",
)}
>
{node.status === "active"
? t("ui.common.status.active", "활성")
: t("ui.common.status.inactive", "비활성")}
</Badge>
</TableCell>
<TableCell className="font-mono text-xs">{node.slug}</TableCell>
<TableCell className="whitespace-nowrap">
<div className="flex items-center gap-2">
<Switch
checked={node.status === "active"}
onCheckedChange={(checked) =>
statusMutation.mutate({
tenantId: node.id,
status: checked ? "active" : "inactive",
})
}
disabled={
statusMutation.isPending ||
node.id === profile?.tenantId ||
isSeedTenant(node)
}
aria-label={t(
"ui.admin.tenants.toggle_status",
"{{name}} 활성 상태",
{ name: node.name },
)}
/>
<span className="text-sm text-muted-foreground">
{t(`ui.common.status.${node.status}`, node.status)}
<div className="flex flex-col leading-tight">
<span className="font-medium">
{t("ui.admin.tenants.table.members_count", "{{count}}명", {
count: node.recursiveMemberCount,
})}
</span>
<span className="mt-0.5 text-xs text-muted-foreground">
{t("ui.admin.tenants.table.members_recursive", "하위 포함")}
</span>
</div>
</TableCell>
<TableCell className="font-medium">
{node.recursiveMemberCount}
</TableCell>
<TableCell className="whitespace-nowrap text-xs">
{node.updatedAt
? new Date(node.updatedAt).toLocaleString("ko-KR")
: "-"}
<TableCell className="whitespace-nowrap">
{node.updatedAt ? (
<div className="flex flex-col leading-tight">
<span className="text-xs">
{new Date(node.updatedAt).toLocaleDateString("ko-KR")}
</span>
<span className="mt-0.5 text-xs text-muted-foreground">
{new Date(node.updatedAt).toLocaleTimeString("ko-KR")}
</span>
</div>
) : (
<span className="text-xs">-</span>
)}
</TableCell>
</TableRow>
);

View File

@@ -1280,7 +1280,9 @@ pick = "Select parent scope"
[ui.admin.tenants.table]
actions = "ACTIONS"
id = "ID"
members_count = "{{count}} members"
members = "Members"
members_recursive = "Includes descendants"
name = "NAME"
slug = "SLUG"
status = "STATUS"

View File

@@ -1283,7 +1283,9 @@ pick = "상위 범위 선택"
[ui.admin.tenants.table]
actions = "ACTIONS"
id = "ID"
members_count = "{{count}}명"
members = "멤버수"
members_recursive = "하위 포함"
name = "이름"
slug = "슬러그"
status = "상태"

View File

@@ -1291,6 +1291,8 @@ slug = ""
status = ""
[ui.admin.tenants.table]
members_count = ""
members_recursive = ""
actions = ""
id = ""
members = ""