diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index 363fe5a7..f564294b 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -1,5 +1,4 @@ -import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query"; -import { useVirtualizer } from "@tanstack/react-virtual"; +import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { Building2, @@ -25,10 +24,14 @@ import { sortableTableHeaderClassName, } from "../../../../../common/core/components/sort"; import { - type SortConfig, - type SortResolverMap, + commonTableShellClass, + commonTableViewportClass, +} from "../../../../../common/ui/table"; +import { sortItems, toggleSort, + type SortConfig, + type SortResolverMap, } from "../../../../../common/core/utils"; import { RoleGuard } from "../../../components/auth/RoleGuard"; import { Badge } from "../../../components/ui/badge"; @@ -76,12 +79,7 @@ import { importTenantsCSV, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; -import { normalizeAdminRole } from "../../../lib/roles"; import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree"; -import { - filterNonHanmacFamilyTenants, - isHanmacFamilyUser, -} from "../../users/orgChartPicker"; import { isSeedTenant } from "../utils/protectedTenants"; import { type TenantImportPreviewRow, @@ -95,14 +93,8 @@ import { const tenantCSVTemplate = "name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n"; -const tenantPageSize = 500; -const tenantVirtualizationThreshold = 250; -const tenantEstimatedRowHeight = 73; -const tenantLoadAheadPx = 360; -const tenantLoadAheadRows = 30; type TenantSortKey = keyof TenantSummary | "recursiveMemberCount"; -type TenantListRow = TenantSummary & { recursiveMemberCount: number }; const getTenantIcon = (type?: string) => { switch (type?.toUpperCase()) { @@ -263,17 +255,15 @@ function TenantListPage() { Record >({}); const [previewOpen, setPreviewOpen] = React.useState(false); - const tenantTableScrollRef = React.useRef(null); const { data: profile } = useQuery({ queryKey: ["me"], queryFn: fetchMe, }); - const profileRole = normalizeAdminRole(profile?.role); // Redirect tenant_admin ONLY if they have one or fewer manageable tenants in the list React.useEffect(() => { - if (profile && profileRole === "tenant_admin") { + if (profile?.role === "tenant_admin") { const manageableCount = profile.manageableTenants?.length ?? 0; if ( (manageableCount === 1 || manageableCount === 0) && @@ -282,24 +272,15 @@ function TenantListPage() { navigate(`/tenants/${profile.tenantId}`, { replace: true }); } } - }, [profile, profileRole, navigate]); + }, [profile, navigate]); - const query = useInfiniteQuery({ - queryKey: ["tenants", "lazy"], - queryFn: ({ pageParam }) => - fetchTenants( - tenantPageSize, - 0, - undefined, - pageParam ? pageParam : undefined, - ), - initialPageParam: "", - getNextPageParam: (lastPage) => - lastPage.nextCursor || lastPage.next_cursor || undefined, + const query = useQuery({ + queryKey: ["tenants", { limit: 1000, offset: 0 }], + queryFn: () => fetchTenants(1000, 0), enabled: - profileRole === "super_admin" || - (profileRole === "tenant_admin" && - (profile?.manageableTenants?.length ?? 0) > 1), + profile?.role === "super_admin" || + (profile?.role === "tenant_admin" && + (profile.manageableTenants?.length ?? 0) > 1), }); const deleteMutation = useMutation({ @@ -364,8 +345,8 @@ function TenantListPage() { if ( profile && - profileRole !== "super_admin" && - profileRole !== "tenant_admin" + profile.role !== "super_admin" && + profile.role !== "tenant_admin" ) { return (
@@ -380,8 +361,7 @@ function TenantListPage() { } if ( - profile && - profileRole === "tenant_admin" && + profile?.role === "tenant_admin" && (profile.manageableTenants?.length ?? 0) <= 1 ) { return null; @@ -394,28 +374,7 @@ function TenantListPage() { ? t("msg.admin.tenants.fetch_error", "테넌트 목록 조회에 실패했습니다.") : null; - const tenantPages = query.data?.pages ?? []; - const rawTenants = tenantPages.flatMap((page) => page.items); - const tenantTotal = tenantPages[0]?.total ?? 0; - const hanmacFamilyTenantId = React.useMemo(() => { - const envTenantId = import.meta.env.VITE_HANMAC_FAMILY_TENANT_ID; - if (typeof envTenantId === "string" && envTenantId.trim()) { - return envTenantId.trim(); - } - return rawTenants.find((tenant) => tenant.slug === "hanmac-family")?.id; - }, [rawTenants]); - const allTenants = React.useMemo(() => { - if (profileRole === "super_admin") { - return rawTenants; - } - if ( - profile && - isHanmacFamilyUser(profile, rawTenants, hanmacFamilyTenantId) - ) { - return rawTenants; - } - return filterNonHanmacFamilyTenants(rawTenants, hanmacFamilyTenantId); - }, [hanmacFamilyTenantId, profile, profileRole, rawTenants]); + const allTenants = query.data?.items ?? []; const importParentOptionGroups = buildTenantImportParentOptionGroups(allTenants); const tenantSortResolvers = React.useMemo< @@ -465,56 +424,6 @@ function TenantListPage() { return sortItems(enriched, sortConfig, tenantSortResolvers); }, [allTenants, search, sortConfig, tenantSortResolvers]); - const shouldVirtualizeTenants = - tenants.length >= tenantVirtualizationThreshold; - const tenantRowVirtualizer = useVirtualizer({ - count: tenants.length, - getScrollElement: () => tenantTableScrollRef.current, - estimateSize: () => tenantEstimatedRowHeight, - overscan: 12, - enabled: shouldVirtualizeTenants, - }); - const virtualTenantRows = shouldVirtualizeTenants - ? tenantRowVirtualizer.getVirtualItems() - : []; - const lastVirtualTenantIndex = - virtualTenantRows[virtualTenantRows.length - 1]?.index ?? -1; - - const fetchNextTenantPage = React.useCallback(() => { - if (query.hasNextPage && !query.isFetchingNextPage) { - void query.fetchNextPage(); - } - }, [query.fetchNextPage, query.hasNextPage, query.isFetchingNextPage]); - - const handleTenantTableScroll = React.useCallback( - (event: React.UIEvent) => { - const scrollElement = event.currentTarget; - const distanceToEnd = - scrollElement.scrollHeight - - scrollElement.scrollTop - - scrollElement.clientHeight; - if (distanceToEnd <= tenantLoadAheadPx) { - fetchNextTenantPage(); - } - }, - [fetchNextTenantPage], - ); - - React.useEffect(() => { - if ( - !shouldVirtualizeTenants || - lastVirtualTenantIndex < tenants.length - tenantLoadAheadRows - ) { - return; - } - fetchNextTenantPage(); - }, [ - fetchNextTenantPage, - lastVirtualTenantIndex, - shouldVirtualizeTenants, - tenants.length, - ]); - const requestSort = (key: TenantSortKey) => { setSortConfig((current) => toggleSort(current, key)); }; @@ -690,96 +599,6 @@ function TenantListPage() { deleteMutation.mutate(tenantId); }; - const renderTenantRow = ( - tenant: TenantListRow, - options?: { - style?: React.CSSProperties; - virtualIndex?: number; - }, - ) => ( - - - {isSeedTenant(tenant) ? ( - - ) : ( - handleSelect(tenant, !!checked)} - /> - )} - - - {tenant.id} - - -
- - {tenant.name} - - {isSeedTenant(tenant) && ( - - {t("ui.admin.tenants.seed_badge", "초기 설정")} - - )} -
-
- - - {tenant.type} - - - {tenant.slug} - - - {t(`ui.common.status.${tenant.status}`, tenant.status)} - - - - {tenant.recursiveMemberCount} - - - {tenant.updatedAt - ? new Date(tenant.updatedAt).toLocaleString("ko-KR") - : "-"} - - - - -
- ); - return (
@@ -908,7 +727,7 @@ function TenantListPage() { "msg.admin.tenants.registry.count", "총 {{count}}개 테넌트", { - count: tenantTotal, + count: query.data?.total ?? 0, }, )} @@ -950,13 +769,8 @@ function TenantListPage() { value="list" className="flex-1 flex flex-col min-h-0 m-0" > -
-
+
+
@@ -1030,18 +844,7 @@ function TenantListPage() { - + {query.isLoading && ( @@ -1062,26 +865,102 @@ function TenantListPage() { )} - {shouldVirtualizeTenants - ? virtualTenantRows.map((virtualRow) => { - const tenant = tenants[virtualRow.index]; - if (!tenant) { - return null; - } - return renderTenantRow(tenant, { - virtualIndex: virtualRow.index, - style: { - position: "absolute", - top: 0, - left: 0, - width: "100%", - display: "table", - tableLayout: "fixed", - transform: `translateY(${virtualRow.start}px)`, - }, - }); - }) - : tenants.map((tenant) => renderTenantRow(tenant))} + {tenants.map((tenant) => ( + + + {isSeedTenant(tenant) ? ( + + ) : ( + + handleSelect(tenant, !!checked) + } + /> + )} + + + {tenant.id} + + +
+ + {tenant.name} + + {isSeedTenant(tenant) && ( + + {t( + "ui.admin.tenants.seed_badge", + "초기 설정", + )} + + )} +
+
+ + + {tenant.type} + + + + {tenant.slug} + + + + {t( + `ui.common.status.${tenant.status}`, + tenant.status, + )} + + + + {tenant.recursiveMemberCount} + + + {tenant.createdAt + ? new Date(tenant.createdAt).toLocaleString( + "ko-KR", + ) + : "-"} + + + + +
+ ))}
diff --git a/adminfront/src/features/users/UserListPage.tsx b/adminfront/src/features/users/UserListPage.tsx index f987838c..3cd8eabc 100644 --- a/adminfront/src/features/users/UserListPage.tsx +++ b/adminfront/src/features/users/UserListPage.tsx @@ -24,6 +24,10 @@ import { sortableTableHeadBaseClassName, sortableTableHeaderClassName, } from "../../../../common/core/components/sort"; +import { + commonTableShellClass, + commonTableViewportClass, +} from "../../../../common/ui/table"; import { Button } from "../../components/ui/button"; import { Card, @@ -555,8 +559,8 @@ function UserListPage() {
)} -
-
+
+