(
+ "/v1/admin/debug/check-permission",
+ {
+ params: { namespace, object, relation, subject },
+ },
+ );
+ return data;
+ },
+ });
+
+ const result = checkMutation.data;
+
+ return (
+
+
+
+
+ ReBAC 권한 검증 도구
+
+
+ 특정 주체(Subject)가 특정 리소스(Object)에 대해 권한이 있는지 Ory
+ Keto를 통해 실시간으로 확인합니다.
+
+
+
+
+
+
+
+
+
+
+ setRelation(e.target.value)}
+ />
+
+
+
+ setObject(e.target.value)}
+ />
+
+
+
+ setSubject(e.target.value)}
+ />
+
+
+
+
+
+
+
+ {checkMutation.isSuccess && (
+
+ {result.allowed ? (
+ <>
+
+
Access ALLOWED
+
+ 해당 사용자는 요청한 리소스에 대해 권한이 있습니다. (상속
+ 포함)
+
+ >
+ ) : (
+ <>
+
+
Access DENIED
+
+ 해당 사용자는 요청한 리소스에 대해 권한이 없습니다.
+
+ >
+ )}
+
+ )}
+
+
+ );
+}
+
+export default PermissionChecker;
diff --git a/adminfront/src/features/tenant-groups/routes/TenantGroupAdminsTab.tsx b/adminfront/src/features/tenant-groups/routes/TenantGroupAdminsTab.tsx
new file mode 100644
index 00000000..794f2e55
--- /dev/null
+++ b/adminfront/src/features/tenant-groups/routes/TenantGroupAdminsTab.tsx
@@ -0,0 +1,215 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { Plus, Search, ShieldCheck, Trash2, UserPlus } from "lucide-react";
+import { useState } from "react";
+import { useOutletContext } from "react-router-dom";
+import { Button } from "../../../components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../../components/ui/card";
+import { Input } from "../../../components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "../../../components/ui/table";
+import {
+ type TenantGroupSummary,
+ addGroupAdmin,
+ fetchGroupAdmins,
+ fetchUsers,
+ removeGroupAdmin,
+} from "../../../lib/adminApi";
+
+function TenantGroupAdminsTab() {
+ const { group } = useOutletContext<{
+ group: TenantGroupSummary;
+ }>();
+ const queryClient = useQueryClient();
+ const [searchTerm, setSearchTerm] = useState("");
+
+ // 현재 관리자 목록
+ const adminsQuery = useQuery({
+ queryKey: ["tenant-group-admins", group.id],
+ queryFn: () => fetchGroupAdmins(group.id),
+ enabled: !!group.id,
+ });
+
+ // 전체 사용자 목록 (관리자 추가용)
+ const usersQuery = useQuery({
+ queryKey: ["users", { limit: 100, search: searchTerm }],
+ queryFn: () => fetchUsers(100, 0, searchTerm),
+ enabled: searchTerm.length > 1, // 2글자 이상 입력 시 검색
+ });
+
+ const addMutation = useMutation({
+ mutationFn: (userId: string) => addGroupAdmin(group.id, userId),
+ onSuccess: () => {
+ adminsQuery.refetch();
+ setSearchTerm("");
+ },
+ });
+
+ const removeMutation = useMutation({
+ mutationFn: (userId: string) => removeGroupAdmin(group.id, userId),
+ onSuccess: () => {
+ adminsQuery.refetch();
+ },
+ });
+
+ const handleAddAdmin = (userId: string) => {
+ addMutation.mutate(userId);
+ };
+
+ const handleRemoveAdmin = (userId: string, userName: string) => {
+ if (window.confirm(`${userName} 사용자의 관리자 권한을 회수할까요?`)) {
+ removeMutation.mutate(userId);
+ }
+ };
+
+ return (
+
+ {/* 현재 그룹 관리자 */}
+
+
+
+
+ 그룹 관리자
+
+
+ 이 그룹과 소속 테넌트를 관리할 수 있는 권한을 가진 사용자들입니다.
+
+
+
+
+
+
+ 이름
+ 이메일
+ 회수
+
+
+
+ {adminsQuery.data?.length === 0 && (
+
+
+ 등록된 관리자가 없습니다.
+
+
+ )}
+ {adminsQuery.data?.map((admin) => (
+
+
+ {admin.name || "Unknown"}
+
+ {admin.email}
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* 사용자 검색 및 추가 */}
+
+
+
+
+
+ 관리자 추가
+
+
+
+ 관리자로 추가할 사용자를 검색하세요 (이름 또는 이메일).
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+ 사용자
+ 추가
+
+
+
+ {searchTerm.length < 2 && (
+
+
+ 사용자 이름을 입력하여 검색하세요.
+
+
+ )}
+ {searchTerm.length >= 2 &&
+ usersQuery.data?.items.length === 0 && (
+
+
+ 검색 결과가 없습니다.
+
+
+ )}
+ {usersQuery.data?.items
+ .filter((u) => !adminsQuery.data?.some((a) => a.id === u.id))
+ .map((user) => (
+
+
+ {user.name}
+
+ {user.email}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export default TenantGroupAdminsTab;
diff --git a/adminfront/src/features/tenant-groups/routes/TenantGroupCreatePage.tsx b/adminfront/src/features/tenant-groups/routes/TenantGroupCreatePage.tsx
new file mode 100644
index 00000000..b9cc1d71
--- /dev/null
+++ b/adminfront/src/features/tenant-groups/routes/TenantGroupCreatePage.tsx
@@ -0,0 +1,144 @@
+import { useMutation } from "@tanstack/react-query";
+import type { AxiosError } from "axios";
+import { LayoutGrid, Sparkles } from "lucide-react";
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { Badge } from "../../../components/ui/badge";
+import { Button } from "../../../components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../../components/ui/card";
+import { Input } from "../../../components/ui/input";
+import { Label } from "../../../components/ui/label";
+import { Textarea } from "../../../components/ui/textarea";
+import { createTenantGroup } from "../../../lib/adminApi";
+
+function TenantGroupCreatePage() {
+ const navigate = useNavigate();
+ const [name, setName] = useState("");
+ const [slug, setSlug] = useState("");
+ const [description, setDescription] = useState("");
+
+ const mutation = useMutation({
+ mutationFn: () =>
+ createTenantGroup({
+ name,
+ slug: slug || name.toLowerCase().replace(/ /g, "-"),
+ description: description || undefined,
+ }),
+ onSuccess: () => {
+ navigate("/tenant-groups");
+ },
+ });
+
+ const errorMsg = (mutation.error as AxiosError<{ error?: string }>)?.response
+ ?.data?.error;
+
+ return (
+
+
+
+
+
+
+
+ Group Profile
+
+
+ 그룹 이름과 식별자(Slug)를 입력합니다.
+
+
+
+
+
+ setName(e.target.value)}
+ placeholder="예: 바론소프트웨어 통합그룹"
+ />
+
+
+
+
setSlug(e.target.value)}
+ placeholder="baron-group"
+ />
+
+ URL이나 API에서 사용될 고유 식별자입니다. 비워두면 이름 기반으로
+ 자동 생성됩니다.
+
+
+
+
+
+
+ {errorMsg && (
+
+ {errorMsg}
+
+ )}
+
+
+
+
+
+
+
+ 권한 상속 안내
+
+
+ 테넌트 그룹의 관리자는 소속된 모든 테넌트에 대한 관리 권한을
+ 자동으로 가집니다.
+
+
+
+ 생성 후 상세 페이지에서 테넌트를 이 그룹에 할당할 수 있습니다.
+
+
+
+
+
+
+
+
+ );
+}
+
+export default TenantGroupCreatePage;
diff --git a/adminfront/src/features/tenant-groups/routes/TenantGroupDetailPage.tsx b/adminfront/src/features/tenant-groups/routes/TenantGroupDetailPage.tsx
new file mode 100644
index 00000000..0bbf3cf0
--- /dev/null
+++ b/adminfront/src/features/tenant-groups/routes/TenantGroupDetailPage.tsx
@@ -0,0 +1,94 @@
+import { useQuery } from "@tanstack/react-query";
+import { ArrowLeft, LayoutGrid } from "lucide-react";
+import { Link, Outlet, useLocation, useParams } from "react-router-dom";
+import { Badge } from "../../../components/ui/badge";
+import { fetchTenantGroup } from "../../../lib/adminApi";
+
+function TenantGroupDetailPage() {
+ const { id } = useParams<{ id: string }>();
+ const location = useLocation();
+
+ const groupQuery = useQuery({
+ queryKey: ["tenant-group", id],
+ queryFn: () => fetchTenantGroup(id!),
+ enabled: !!id,
+ });
+
+ const isTenantsTab = location.pathname.endsWith("/tenants");
+ const isAdminTab = location.pathname.endsWith("/admins");
+
+ return (
+
+
+
+ {/* Tabs */}
+
+
+ 기본 정보
+
+
+ 소속 테넌트 ({groupQuery.data?.tenants?.length ?? 0})
+
+
+ 관리자
+
+
+
+
+
+
+
+ );
+}
+
+export default TenantGroupDetailPage;
diff --git a/adminfront/src/features/tenant-groups/routes/TenantGroupListPage.tsx b/adminfront/src/features/tenant-groups/routes/TenantGroupListPage.tsx
new file mode 100644
index 00000000..cdb8ed64
--- /dev/null
+++ b/adminfront/src/features/tenant-groups/routes/TenantGroupListPage.tsx
@@ -0,0 +1,172 @@
+import { useMutation, useQuery } from "@tanstack/react-query";
+import type { AxiosError } from "axios";
+import { LayoutGrid, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
+import { Link, useNavigate } from "react-router-dom";
+import { Badge } from "../../../components/ui/badge";
+import { Button } from "../../../components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../../components/ui/card";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "../../../components/ui/table";
+import { deleteTenantGroup, fetchTenantGroups } from "../../../lib/adminApi";
+
+function TenantGroupListPage() {
+ const navigate = useNavigate();
+ const query = useQuery({
+ queryKey: ["tenant-groups", { limit: 50, offset: 0 }],
+ queryFn: () => fetchTenantGroups(50, 0),
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: (groupId: string) => deleteTenantGroup(groupId),
+ onSuccess: () => {
+ query.refetch();
+ },
+ });
+
+ const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response
+ ?.data?.error;
+ const fallbackError =
+ !errorMsg && query.isError ? "테넌트 그룹 목록 조회에 실패했습니다." : null;
+
+ const items = query.data?.items ?? [];
+
+ const handleDelete = (groupId: string, groupName: string) => {
+ if (!window.confirm(`테넌트 그룹 "${groupName}"를 삭제할까요?`)) {
+ return;
+ }
+ deleteMutation.mutate(groupId);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ Tenant Group Registry
+
+
+ 총 {query.data?.total ?? 0}개 그룹
+
+
+ Super Admin only
+
+
+ {(errorMsg || fallbackError) && (
+
+ {errorMsg ?? fallbackError}
+
+ )}
+
+
+
+
+ NAME
+ SLUG
+ TENANTS
+ CREATED
+ ACTIONS
+
+
+
+ {query.isLoading && (
+
+ 로딩 중...
+
+ )}
+ {!query.isLoading && items.length === 0 && (
+
+
+ 아직 등록된 테넌트 그룹이 없습니다.
+
+
+ )}
+ {items.map((group) => (
+
+ {group.name}
+ {group.slug}
+
+
+ {group.tenants?.length ?? 0}개
+
+
+
+ {group.createdAt
+ ? new Date(group.createdAt).toLocaleDateString("ko-KR")
+ : "-"}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export default TenantGroupListPage;
diff --git a/adminfront/src/features/tenant-groups/routes/TenantGroupProfileTab.tsx b/adminfront/src/features/tenant-groups/routes/TenantGroupProfileTab.tsx
new file mode 100644
index 00000000..81b00aa0
--- /dev/null
+++ b/adminfront/src/features/tenant-groups/routes/TenantGroupProfileTab.tsx
@@ -0,0 +1,104 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import type { AxiosError } from "axios";
+import { useState } from "react";
+import { useOutletContext } from "react-router-dom";
+import { Button } from "../../../components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../../components/ui/card";
+import { Input } from "../../../components/ui/input";
+import { Label } from "../../../components/ui/label";
+import { Textarea } from "../../../components/ui/textarea";
+import {
+ type TenantGroupSummary,
+ updateTenantGroup,
+} from "../../../lib/adminApi";
+
+function TenantGroupProfileTab() {
+ const { group, refetch } = useOutletContext<{
+ group: TenantGroupSummary;
+ refetch: () => void;
+ }>();
+ const queryClient = useQueryClient();
+
+ const [name, setName] = useState(group?.name ?? "");
+ const [description, setDescription] = useState(group?.description ?? "");
+
+ const mutation = useMutation({
+ mutationFn: () => updateTenantGroup(group.id, { name, description }),
+ onSuccess: () => {
+ refetch();
+ queryClient.invalidateQueries({ queryKey: ["tenant-groups"] });
+ },
+ });
+
+ const errorMsg = (mutation.error as AxiosError<{ error?: string }>)?.response
+ ?.data?.error;
+
+ if (!group) return null;
+
+ return (
+
+ );
+}
+
+export default TenantGroupProfileTab;
diff --git a/adminfront/src/features/tenant-groups/routes/TenantGroupTenantsTab.tsx b/adminfront/src/features/tenant-groups/routes/TenantGroupTenantsTab.tsx
new file mode 100644
index 00000000..a418e4d7
--- /dev/null
+++ b/adminfront/src/features/tenant-groups/routes/TenantGroupTenantsTab.tsx
@@ -0,0 +1,210 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { Building2, Plus, Search, Trash2 } from "lucide-react";
+import { useState } from "react";
+import { useOutletContext } from "react-router-dom";
+import { Badge } from "../../../components/ui/badge";
+import { Button } from "../../../components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../../components/ui/card";
+import { Input } from "../../../components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "../../../components/ui/table";
+import {
+ type TenantGroupSummary,
+ addTenantToGroup,
+ fetchTenants,
+ removeTenantFromGroup,
+} from "../../../lib/adminApi";
+
+function TenantGroupTenantsTab() {
+ const { group, refetch } = useOutletContext<{
+ group: TenantGroupSummary;
+ refetch: () => void;
+ }>();
+ const queryClient = useQueryClient();
+ const [searchTerm, setSearchTerm] = useState("");
+
+ // 전체 테넌트 목록 (할당용)
+ const tenantsQuery = useQuery({
+ queryKey: ["tenants", { limit: 100 }],
+ queryFn: () => fetchTenants(100, 0),
+ });
+
+ const addMutation = useMutation({
+ mutationFn: (tenantId: string) => addTenantToGroup(group.id, tenantId),
+ onSuccess: () => {
+ refetch();
+ queryClient.invalidateQueries({ queryKey: ["tenant-group", group.id] });
+ },
+ });
+
+ const removeMutation = useMutation({
+ mutationFn: (tenantId: string) => removeTenantFromGroup(group.id, tenantId),
+ onSuccess: () => {
+ refetch();
+ queryClient.invalidateQueries({ queryKey: ["tenant-group", group.id] });
+ },
+ });
+
+ const handleAddTenant = (tenantId: string) => {
+ addMutation.mutate(tenantId);
+ };
+
+ const handleRemoveTenant = (tenantId: string) => {
+ if (window.confirm("이 테넌트를 그룹에서 제외할까요?")) {
+ removeMutation.mutate(tenantId);
+ }
+ };
+
+ const availableTenants =
+ tenantsQuery.data?.items.filter(
+ (t) => !group.tenants?.some((gt) => gt.id === t.id),
+ ) || [];
+
+ const filteredAvailable = availableTenants.filter(
+ (t) =>
+ t.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ t.slug.toLowerCase().includes(searchTerm.toLowerCase()),
+ );
+
+ return (
+
+ {/* 현재 소속 테넌트 */}
+
+
+
+
+ 소속 테넌트
+
+
+ 현재 이 그룹에 포함된 테넌트 목록입니다.
+
+
+
+
+
+
+ 이름
+ Slug
+ 제외
+
+
+
+ {group.tenants?.length === 0 && (
+
+
+ 소속된 테넌트가 없습니다.
+
+
+ )}
+ {group.tenants?.map((t) => (
+
+ {t.name}
+ {t.slug}
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* 추가 가능한 테넌트 */}
+
+
+
+
+
+ 테넌트 추가
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+ 다른 그룹에 속하지 않았거나 이동이 필요한 테넌트를 선택하세요.
+
+
+
+
+
+
+ 이름
+ 상태
+ 추가
+
+
+
+ {filteredAvailable.length === 0 && (
+
+
+ 추가할 수 있는 테넌트가 없습니다.
+
+
+ )}
+ {filteredAvailable.map((t) => (
+
+
+ {t.name}
+
+ {t.slug}
+
+
+
+
+ {t.status}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export default TenantGroupTenantsTab;
diff --git a/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx b/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx
new file mode 100644
index 00000000..e10979b8
--- /dev/null
+++ b/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx
@@ -0,0 +1,214 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { Plus, Search, ShieldCheck, Trash2, UserPlus } from "lucide-react";
+import { useState } from "react";
+import { useParams } from "react-router-dom";
+import { Button } from "../../../components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../../components/ui/card";
+import { Input } from "../../../components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "../../../components/ui/table";
+import {
+ addTenantAdmin,
+ fetchTenantAdmins,
+ fetchUsers,
+ removeTenantAdmin,
+} from "../../../lib/adminApi";
+
+function TenantAdminsTab() {
+ const { tenantId } = useParams<{ tenantId: string }>();
+ const queryClient = useQueryClient();
+ const [searchTerm, setSearchTerm] = useState("");
+
+ if (!tenantId) return null;
+
+ // 현재 관리자 목록
+ const adminsQuery = useQuery({
+ queryKey: ["tenant-admins", tenantId],
+ queryFn: () => fetchTenantAdmins(tenantId),
+ enabled: !!tenantId,
+ });
+
+ // 전체 사용자 목록 (관리자 추가용)
+ const usersQuery = useQuery({
+ queryKey: ["users", { limit: 100, search: searchTerm }],
+ queryFn: () => fetchUsers(100, 0, searchTerm),
+ enabled: searchTerm.length > 1,
+ });
+
+ const addMutation = useMutation({
+ mutationFn: (userId: string) => addTenantAdmin(tenantId, userId),
+ onSuccess: () => {
+ adminsQuery.refetch();
+ setSearchTerm("");
+ },
+ });
+
+ const removeMutation = useMutation({
+ mutationFn: (userId: string) => removeTenantAdmin(tenantId, userId),
+ onSuccess: () => {
+ adminsQuery.refetch();
+ },
+ });
+
+ const handleAddAdmin = (userId: string) => {
+ addMutation.mutate(userId);
+ };
+
+ const handleRemoveAdmin = (userId: string, userName: string) => {
+ if (window.confirm(`${userName} 사용자의 관리자 권한을 회수할까요?`)) {
+ removeMutation.mutate(userId);
+ }
+ };
+
+ return (
+
+ {/* 현재 테넌트 관리자 */}
+
+
+
+
+ 테넌트 관리자
+
+
+ 이 테넌트의 자원을 관리할 수 있는 권한을 가진 사용자들입니다.
+
+
+
+
+
+
+ 이름
+ 이메일
+ 회수
+
+
+
+ {adminsQuery.data?.length === 0 && (
+
+
+ 등록된 관리자가 없습니다.
+
+
+ )}
+ {adminsQuery.data?.map((admin) => (
+
+
+ {admin.name || "Unknown"}
+
+ {admin.email}
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* 사용자 검색 및 추가 */}
+
+
+
+
+
+ 관리자 추가
+
+
+
+ 관리자로 추가할 사용자를 검색하세요 (이름 또는 이메일).
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+ 사용자
+ 추가
+
+
+
+ {searchTerm.length < 2 && (
+
+
+ 사용자 이름을 입력하여 검색하세요.
+
+
+ )}
+ {searchTerm.length >= 2 &&
+ usersQuery.data?.items.length === 0 && (
+
+
+ 검색 결과가 없습니다.
+
+
+ )}
+ {usersQuery.data?.items
+ .filter((u) => !adminsQuery.data?.some((a) => a.id === u.id))
+ .map((user) => (
+
+
+ {user.name}
+
+ {user.email}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export default TenantAdminsTab;
diff --git a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx
index eb087b00..f1ea6b92 100644
--- a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx
+++ b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx
@@ -16,6 +16,7 @@ function TenantDetailPage() {
});
const isFederationTab = location.pathname.includes("/federation");
+ const isAdminTab = location.pathname.includes("/admins");
return (
@@ -44,7 +45,9 @@ function TenantDetailPage() {
Federation
+
+ Admins
+
fetchTenant(tenantId),
});
+ const groupsQuery = useQuery({
+ queryKey: ["tenant-groups", { limit: 100 }],
+ queryFn: () => fetchTenantGroups(100, 0),
+ });
+
const [name, setName] = useState("");
const [slug, setSlug] = useState("");
const [description, setDescription] = useState("");
const [status, setStatus] = useState("active");
const [domains, setDomains] = useState("");
+ const [tenantGroupId, setTenantGroupId] = useState("");
useEffect(() => {
if (tenantQuery.data) {
@@ -48,6 +55,7 @@ export function TenantProfilePage() {
setDescription(tenantQuery.data.description ?? "");
setStatus(tenantQuery.data.status);
setDomains(tenantQuery.data.domains?.join(", ") ?? "");
+ setTenantGroupId(tenantQuery.data.tenantGroupId ?? "");
}
}, [tenantQuery.data]);
@@ -58,6 +66,7 @@ export function TenantProfilePage() {
slug,
description: description || undefined,
status,
+ tenantGroupId: tenantGroupId || undefined,
domains: domains
.split(",")
.map((d) => d.trim())
@@ -136,6 +145,25 @@ export function TenantProfilePage() {
onChange={(e) => setDescription(e.target.value)}
/>
+
+
+
+
+ 테넌트가 속할 그룹을 지정합니다. 그룹 관리자는 소속 테넌트에 대한
+ 접근 권한을 가집니다.
+
+