From 3dcdd9788293efe90d0b3d0302c92d9ca37ff7ea Mon Sep 17 00:00:00 2001 From: Lectom Date: Thu, 30 Apr 2026 17:02:24 +0900 Subject: [PATCH] =?UTF-8?q?org=20chart=20=EC=9E=90=EB=8F=99=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=B3=B4=EC=99=84.=20seed-tenant=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=B6=88=EA=B0=80=20=EC=A1=B0=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tenants/routes/TenantListPage.tsx | 74 ++++++-- .../tenants/routes/TenantProfilePage.tsx | 17 +- .../tenants/utils/protectedTenants.test.ts | 12 ++ .../tenants/utils/protectedTenants.ts | 19 +++ .../tests/tenant_seed_protection.spec.ts | 117 +++++++++++++ backend/internal/bootstrap/tenant_seed.go | 30 ++++ .../internal/bootstrap/tenant_seed_test.go | 18 ++ backend/internal/handler/auth_handler.go | 21 ++- .../handler/auth_handler_linked_test.go | 2 +- backend/internal/handler/tenant_handler.go | 36 +++- .../tenant_handler_seed_delete_test.go | 159 ++++++++++++++++++ orgfront/src/features/auth/LoginPage.tsx | 2 +- orgfront/tests/orgfront-auto-login.spec.ts | 15 +- 13 files changed, 490 insertions(+), 32 deletions(-) create mode 100644 adminfront/src/features/tenants/utils/protectedTenants.test.ts create mode 100644 adminfront/src/features/tenants/utils/protectedTenants.ts create mode 100644 adminfront/tests/tenant_seed_protection.spec.ts create mode 100644 backend/internal/handler/tenant_handler_seed_delete_test.go diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index dc13cb1d..4a724071 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -57,6 +57,7 @@ import { parseTenantCSV, serializeTenantImportCSV, } from "../utils/tenantCsvImport"; +import { isSeedTenant } from "../utils/protectedTenants"; const tenantCSVTemplate = "name,type,parent_tenant_slug,slug,memo,email_domain\n"; @@ -206,36 +207,48 @@ function TenantListPage() { ); }, [allTenants, search]); + const deletableTenants = React.useMemo( + () => tenants.filter((tenant) => !isSeedTenant(tenant)), + [tenants], + ); + const handleSelectAll = (checked: boolean) => { if (checked) { - setSelectedIds(tenants.map((t) => t.id)); + setSelectedIds(deletableTenants.map((t) => t.id)); } else { setSelectedIds([]); } }; - const handleSelect = (id: string, checked: boolean) => { + const handleSelect = (tenant: TenantSummary, checked: boolean) => { + if (isSeedTenant(tenant)) { + return; + } if (checked) { - setSelectedIds((prev) => [...prev, id]); + setSelectedIds((prev) => [...prev, tenant.id]); } else { - setSelectedIds((prev) => prev.filter((i) => i !== id)); + setSelectedIds((prev) => prev.filter((i) => i !== tenant.id)); } }; const handleDeleteBulk = () => { if (selectedIds.length === 0) return; + const deletableIds = selectedIds.filter((id) => + deletableTenants.some((tenant) => tenant.id === id), + ); + if (deletableIds.length === 0) return; if ( !window.confirm( t( "msg.admin.tenants.delete_bulk_confirm", "선택한 {{count}}개 테넌트를 삭제할까요?", - { count: selectedIds.length }, + { count: deletableIds.length }, ), ) ) { return; } - deleteBulkMutation.mutate(selectedIds); + deleteBulkMutation.mutate(deletableIds); }; const handleTemplateDownload = () => { @@ -312,6 +325,10 @@ function TenantListPage() { }; const handleDelete = (tenantId: string, tenantName: string) => { + const tenant = allTenants.find((item) => item.id === tenantId); + if (tenant && isSeedTenant(tenant)) { + return; + } if ( !window.confirm( t( @@ -473,7 +490,8 @@ function TenantListPage() { 0 && - selectedIds.length === tenants.length + deletableTenants.length > 0 && + selectedIds.length === deletableTenants.length } onCheckedChange={(checked) => handleSelectAll(!!checked) @@ -529,13 +547,17 @@ function TenantListPage() { )} {tenants.map((tenant) => ( - - - handleSelect(tenant.id, !!checked) - } - /> + + {isSeedTenant(tenant) ? ( + + ) : ( + + handleSelect(tenant, !!checked) + } + /> + )} - {tenant.name} +
+ {tenant.name} + {isSeedTenant(tenant) && ( + + {t( + "ui.admin.tenants.seed_badge", + "초기 설정", + )} + + )} +
handleDelete(tenant.id, tenant.name)} - disabled={deleteMutation.isPending} + disabled={ + deleteMutation.isPending || isSeedTenant(tenant) + } + title={ + isSeedTenant(tenant) + ? t( + "msg.admin.tenants.seed_delete_blocked", + "초기 설정 테넌트는 삭제할 수 없습니다.", + ) + : undefined + } > {t("ui.common.delete", "삭제")} diff --git a/adminfront/src/features/tenants/routes/TenantProfilePage.tsx b/adminfront/src/features/tenants/routes/TenantProfilePage.tsx index 5b807ffd..9bcfbe15 100644 --- a/adminfront/src/features/tenants/routes/TenantProfilePage.tsx +++ b/adminfront/src/features/tenants/routes/TenantProfilePage.tsx @@ -28,6 +28,7 @@ import { formatDomainConflictMessage, type ServerDomainConflict, } from "../utils/domainTags"; +import { isSeedTenant } from "../utils/protectedTenants"; export function TenantProfilePage() { const { tenantId } = useParams<{ tenantId: string }>(); @@ -154,8 +155,14 @@ export function TenantProfilePage() { ?.response?.data?.error; const loadError = (tenantQuery.error as AxiosError<{ error?: string }>) ?.response?.data?.error; + const isProtectedSeedTenant = tenantQuery.data + ? isSeedTenant(tenantQuery.data) + : false; const handleDelete = () => { + if (isProtectedSeedTenant) { + return; + } if ( window.confirm( t("msg.admin.tenants.delete_confirm", "삭제하시겠습니까?", { @@ -335,7 +342,15 @@ export function TenantProfilePage() {