From f6c7cb3b225e727df197a5c957a25022730421a6 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 4 Jun 2026 15:15:05 +0900 Subject: [PATCH] =?UTF-8?q?tenants=20=EB=A0=88=EC=A7=80=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=80=EB=8F=85=EC=84=B1/=EB=A1=9C=EC=BC=80?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tenants/routes/TenantListPage.tsx | 96 +++++++++++++------ adminfront/src/locales/en.toml | 10 ++ adminfront/src/locales/ko.toml | 18 +++- locales/en.toml | 15 +++ locales/ko.toml | 23 ++++- locales/template.toml | 14 +++ 6 files changed, 138 insertions(+), 38 deletions(-) diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index 96646a18..6d99ddae 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -69,7 +69,6 @@ import { SelectTrigger, SelectValue, } from "../../../components/ui/select"; -import { Switch } from "../../../components/ui/switch"; import { Table, TableBody, @@ -80,7 +79,6 @@ import { } from "../../../components/ui/table"; import { Tabs, TabsList, TabsTrigger } from "../../../components/ui/tabs"; import { toast } from "../../../components/ui/use-toast"; -import type { UserProfileResponse } from "../../../lib/adminApi"; import { deleteTenantsBulk, exportTenantsCSV, @@ -142,6 +140,51 @@ const getTenantIcon = (type?: string) => { } }; +function getTenantTypeLabel(type?: string) { + if (!type) return "-"; + return t(`domain.tenant_type.${type.toLowerCase()}`, type); +} + +function getTenantTypeTextClass(type?: string) { + switch (type?.toUpperCase()) { + case "COMPANY_GROUP": + return "text-sky-700"; + case "COMPANY": + return "text-violet-700"; + case "ORGANIZATION": + return "text-emerald-700"; + case "USER_GROUP": + return "text-amber-700"; + case "PERSONAL": + return "text-slate-700"; + default: + return "text-muted-foreground"; + } +} + +function buildTenantParentPathMap(tenants: TenantSummary[]) { + const tenantById = new Map(tenants.map((tenant) => [tenant.id, tenant])); + const pathMap = new Map(); + + for (const tenant of tenants) { + const names: string[] = []; + const visited = new Set(); + let currentParentId = tenant.parentId; + + while (currentParentId && !visited.has(currentParentId)) { + visited.add(currentParentId); + const parent = tenantById.get(currentParentId); + if (!parent) break; + names.unshift(parent.name); + currentParentId = parent.parentId; + } + + pathMap.set(tenant.id, names); + } + + return pathMap; +} + const noImportParentRef = "__none__"; function tenantParentRef(tenantId: string) { @@ -339,19 +382,6 @@ function TenantListPage() { }, }); - const statusMutation = useMutation({ - mutationFn: ({ tenantId, status }: { tenantId: string; status: string }) => - updateTenant(tenantId, { status }), - onSuccess: () => { - query.refetch(); - }, - onError: () => { - toast.error( - t("msg.admin.tenants.status_error", "테넌트 상태 변경에 실패했습니다."), - ); - }, - }); - const bulkUpdateStatusMutation = useMutation({ mutationFn: async ({ tenantIds, @@ -935,8 +965,6 @@ function TenantListPage() { onSelectAll={handleSelectAll} search={search} deletableTenants={deletableTenants} - statusMutation={statusMutation} - profile={profile} sortConfig={sortConfig} requestSort={requestSort} getSortIcon={getSortIcon} @@ -1513,13 +1541,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 | null; requestSort: (key: TenantSortKey) => void; getSortIcon: (key: TenantSortKey) => React.ReactNode; @@ -1536,8 +1557,6 @@ const TenantHierarchyView: React.FC<{ onSelectAll, search, deletableTenants, - statusMutation, - profile, sortConfig, requestSort, getSortIcon, @@ -1558,6 +1577,10 @@ const TenantHierarchyView: React.FC<{ () => buildTenantFullTree(tenants, scopeTenantId || undefined, !!search), [scopeTenantId, tenants, search], ); + const tenantParentPathMap = React.useMemo( + () => buildTenantParentPathMap(tenants), + [tenants], + ); // Initial expanded state: everything open const [expandedIds, setExpandedIds] = React.useState>(() => { @@ -1682,6 +1705,22 @@ const TenantHierarchyView: React.FC<{ const visibleSelectedCount = selectedIds.filter((id) => visibleSelectableIds.has(id), ).length; + const normalizedSearch = search.trim(); + const emptyMessage = React.useMemo(() => { + if (normalizedSearch) { + return t( + "msg.admin.tenants.empty_search", + "검색 조건에 맞는 테넌트가 없습니다.", + ); + } + if (scopeTenantId) { + return t( + "msg.admin.tenants.empty_scope", + "선택한 범위에 표시할 하위 테넌트가 없습니다.", + ); + } + return t("msg.admin.tenants.empty", "아직 등록된 테넌트가 없습니다."); + }, [normalizedSearch, scopeTenantId]); const renderRow = ( node: TenantViewRow, @@ -1923,10 +1962,7 @@ const TenantHierarchyView: React.FC<{ colSpan={8} className="py-8 text-center text-muted-foreground" > - {t( - "msg.admin.tenants.empty", - "아직 등록된 테넌트가 없습니다.", - )} + {emptyMessage} )} diff --git a/adminfront/src/locales/en.toml b/adminfront/src/locales/en.toml index ffb10bb1..abe23e4f 100644 --- a/adminfront/src/locales/en.toml +++ b/adminfront/src/locales/en.toml @@ -1071,6 +1071,7 @@ user = "General User (Tenant Member)" [ui.admin.tenants] add = "Add Tenant" csv_template = "Template" +data_mgmt = "Data Management" delete_selected = "Delete Selected" export_with_ids = "Include UUIDs" export_without_ids = "Export without UUIDs" @@ -1267,6 +1268,15 @@ name = "NAME" slug = "SLUG" status = "STATUS" +[ui.admin.tenants.view] +list = "List" +table = "Table" +tree = "Tree" + +[ui.admin.tenants.scope] +active = "{{name}} descendants" +pick = "Select parent scope" + [ui.admin.tenants.table] actions = "ACTIONS" id = "ID" diff --git a/adminfront/src/locales/ko.toml b/adminfront/src/locales/ko.toml index 03f6502b..8f9d3694 100644 --- a/adminfront/src/locales/ko.toml +++ b/adminfront/src/locales/ko.toml @@ -1074,6 +1074,7 @@ user = "일반 사용자 (Tenant Member)" [ui.admin.tenants] add = "테넌트 추가" csv_template = "템플릿" +data_mgmt = "데이터 관리" delete_selected = "선택 삭제" export_with_ids = "UUID 포함" export_without_ids = "UUID 제외 내보내기" @@ -1270,15 +1271,24 @@ name = "NAME" slug = "SLUG" status = "STATUS" +[ui.admin.tenants.view] +list = "평면 목록" +table = "평면" +tree = "트리" + +[ui.admin.tenants.scope] +active = "{{name}} 하위" +pick = "상위 범위 선택" + [ui.admin.tenants.table] actions = "ACTIONS" id = "ID" members = "멤버수" -name = "NAME" -slug = "SLUG" -status = "STATUS" +name = "이름" +slug = "슬러그" +status = "상태" type = "유형" -updated = "UPDATED" +updated = "수정일" [ui.admin.users] csv_template = "템플릿 다운로드" diff --git a/locales/en.toml b/locales/en.toml index 6c33882b..215edaf2 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -264,6 +264,15 @@ subtitle = "List of owners with top-level permissions for this tenant." [msg.admin.tenants.registry] count = "{{count}} tenants loaded." +scope_results = "{{count}} tenants under {{name}}" +scope_search_results = "{{count}} search results under {{name}}" +search_results = "{{count}} search results" +table_hint = "Compare IDs, status, and size quickly in the sortable flat list." +tree_hint = "Review parent-child relationships and subtree coverage in the hierarchy." + +[msg.admin.tenants] +empty_scope = "There are no child tenants to display in the selected scope." +empty_search = "No tenants match the current search." [msg.admin.tenants.schema] empty = "No custom fields defined. Click \\\\\\\"Add Field\\\\\\\" to begin." @@ -1157,6 +1166,7 @@ user = "TENANT MEMBER" [ui.admin.tenants] add = "Add Tenant" csv_template = "Template" +data_mgmt = "Data Management" delete_selected = "Delete Selected" export_with_ids = "Include UUIDs" export_without_ids = "Export without UUIDs" @@ -1441,9 +1451,14 @@ status = "STATUS" [ui.admin.tenants.table] actions = "ACTIONS" +context = "Parent Path" id = "ID" +id_copy = "Copy ID" members = "Members" +members_count = "{{count}} members" +members_recursive = "including descendants" name = "NAME" +root = "Top Level" slug = "SLUG" status = "STATUS" type = "TYPE" diff --git a/locales/ko.toml b/locales/ko.toml index f2655ee2..f9a0f288 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -765,6 +765,15 @@ subtitle = "이 테넌트의 최상위 권한을 가진 소유자(조직장) 목 [msg.admin.tenants.registry] count = "총 {{count}}개 테넌트" +scope_results = "{{name}} 하위 {{count}}개" +scope_search_results = "{{name}} 하위 검색 결과 {{count}}개" +search_results = "검색 결과 {{count}}개" +table_hint = "정렬 가능한 평면 목록에서 ID, 상태, 규모를 빠르게 비교합니다." +tree_hint = "계층 구조를 따라 부모-자식 관계와 하위 범위를 함께 확인합니다." + +[msg.admin.tenants] +empty_scope = "선택한 범위에 표시할 하위 테넌트가 없습니다." +empty_search = "검색 조건에 맞는 테넌트가 없습니다." [msg.admin.tenants.schema] empty = "등록된 커스텀 필드가 없습니다. 필드 추가를 눌러 시작하세요." @@ -1652,6 +1661,7 @@ user = "TENANT MEMBER" [ui.admin.tenants] add = "테넌트 추가" +data_mgmt = "데이터 관리" delete_selected = "선택 삭제" seed_badge = "초기 설정" title = "테넌트 목록" @@ -1904,13 +1914,18 @@ status = "STATUS" [ui.admin.tenants.table] actions = "ACTIONS" +context = "상위 경로" id = "ID" +id_copy = "ID 복사" members = "멤버수" -name = "NAME" -slug = "SLUG" -status = "STATUS" +members_count = "{{count}}명" +members_recursive = "하위 포함" +name = "이름" +root = "최상위" +slug = "슬러그" +status = "상태" type = "유형" -updated = "UPDATED" +updated = "수정일" created = "CREATED" created = "CREATED" diff --git a/locales/template.toml b/locales/template.toml index eac89aca..c0f26def 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -622,6 +622,15 @@ subtitle = "" [msg.admin.tenants.registry] count = "" +scope_results = "" +scope_search_results = "" +search_results = "" +table_hint = "" +tree_hint = "" + +[msg.admin.tenants] +empty_scope = "" +empty_search = "" [msg.admin.tenants.schema] empty = "" @@ -1781,9 +1790,14 @@ status = "" [ui.admin.tenants.table] actions = "" +context = "" id = "" +id_copy = "" members = "" +members_count = "" +members_recursive = "" name = "" +root = "" slug = "" status = "" type = ""