diff --git a/adminfront/src/app/routes.tsx b/adminfront/src/app/routes.tsx
index 472d42e4..5603a013 100644
--- a/adminfront/src/app/routes.tsx
+++ b/adminfront/src/app/routes.tsx
@@ -18,6 +18,7 @@ import TenantListPage from "../features/tenants/routes/TenantListPage";
import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage";
import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage";
import { TenantFineGrainedPermissionsTab } from "../features/tenants/routes/TenantFineGrainedPermissionsTab";
+import { TenantFineGrainedPermissionsPage } from "../features/tenants/routes/TenantFineGrainedPermissionsPage";
import { TenantWorksmobilePage } from "../features/tenants/routes/TenantWorksmobilePage";
import TenantUserGroupsTab from "../features/user-groups/routes/TenantUserGroupsTab";
import GlobalCustomClaimsPage from "../features/users/GlobalCustomClaimsPage";
@@ -52,6 +53,7 @@ export const adminRoutes: RouteObject[] = [
{ path: "tenants", element: },
{ path: "tenants/new", element: },
{ path: "worksmobile", element: },
+ { path: "permissions-direct", element: },
{
path: "tenants/:tenantId",
element: ,
diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx
index 3cf9fcfd..d378f9e1 100644
--- a/adminfront/src/components/layout/AppLayout.tsx
+++ b/adminfront/src/components/layout/AppLayout.tsx
@@ -62,6 +62,12 @@ const staticNavItems: ShellSidebarNavItem[] = [
to: "/users",
icon: Users,
},
+ {
+ labelKey: "ui.admin.nav.permissions_direct",
+ labelFallback: "Direct Permissions",
+ to: "/permissions-direct",
+ icon: ShieldCheck,
+ },
{
labelKey: "ui.admin.nav.auth_guard",
labelFallback: "Auth Guard",
@@ -208,6 +214,7 @@ function AppLayout() {
});
const filteredItems = items.filter((item) => {
if (item.to === "/api-keys") return isSuperAdmin;
+ if (item.to === "/permissions-direct") return isSuperAdmin || _manageableCount > 0;
return true;
});
diff --git a/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsPage.tsx b/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsPage.tsx
new file mode 100644
index 00000000..cc7cfaca
--- /dev/null
+++ b/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsPage.tsx
@@ -0,0 +1,87 @@
+import { useQuery } from "@tanstack/react-query";
+import { useState } from "react";
+import { fetchAllTenants, fetchMe } from "../../../lib/adminApi";
+import { t } from "../../../lib/i18n";
+import { TenantFineGrainedPermissionsTab } from "./TenantFineGrainedPermissionsTab";
+import { ShieldCheck } from "lucide-react";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../../components/ui/card";
+
+export function TenantFineGrainedPermissionsPage() {
+ const [selectedTenantId, setSelectedTenantId] = useState("");
+
+ const { data: profile } = useQuery({
+ queryKey: ["me"],
+ queryFn: fetchMe,
+ });
+
+ const isSuperAdmin = profile?.role === "super_admin";
+
+ const tenantsQuery = useQuery({
+ queryKey: ["tenants", "list-all"],
+ queryFn: () => fetchAllTenants(),
+ enabled: isSuperAdmin,
+ });
+
+ const tenants = isSuperAdmin
+ ? (tenantsQuery.data?.items ?? [])
+ : (profile?.manageableTenants ?? []);
+
+ return (
+
+
+
+
+ {t("ui.admin.nav.permissions_direct", "권한 부여")}
+
+
+ {t(
+ "msg.admin.permissions_direct.description",
+ "선택한 테넌트의 각 기능별 세부 조회 및 수정 권한을 지정하고 부여합니다.",
+ )}
+
+
+
+
+
+
+ {t("ui.admin.permissions_direct.select_tenant", "대상 테넌트 선택")}
+
+
+ {t(
+ "msg.admin.permissions_direct.select_tenant_desc",
+ "권한을 부여할 대상 테넌트를 리스트에서 선택해 주세요.",
+ )}
+
+
+
+
+
+
+
+ {selectedTenantId ? (
+
+ ) : (
+
+ {t("msg.admin.permissions_direct.select_prompt", "상단에서 테넌트를 선택하면 세부 권한 격리 설정 격자가 노출됩니다.")}
+
+ )}
+
+ );
+}
diff --git a/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsTab.tsx b/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsTab.tsx
index d2020ce5..9f801e8a 100644
--- a/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsTab.tsx
+++ b/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsTab.tsx
@@ -45,9 +45,13 @@ import {
import { t } from "../../../lib/i18n";
import { Trash2 } from "lucide-react";
-export function TenantFineGrainedPermissionsTab() {
+interface TenantFineGrainedPermissionsTabProps {
+ tenantIdProp?: string;
+}
+
+export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrainedPermissionsTabProps = {}) {
const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
- const tenantId = tenantIdParam ?? "";
+ const tenantId = tenantIdProp || tenantIdParam || "";
const { hasPermission } = useTenantPermission(tenantId);
const isWritable = hasPermission("manage_admins");
const queryClient = useQueryClient();