From fd6addfffda12a2a51141034f53db96ea3cc1e68 Mon Sep 17 00:00:00 2001 From: chan Date: Wed, 10 Jun 2026 17:38:52 +0900 Subject: [PATCH] =?UTF-8?q?adminfront:=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=84=A4=EC=A0=95=20=ED=8C=A8=EB=84=90?= =?UTF-8?q?=EC=97=90=20=EC=8A=AC=EB=9E=99=20=EC=8A=A4=ED=83=80=EC=9D=BC?= =?UTF-8?q?=EC=9D=98=20=EC=A2=8C=EC=9A=B0=20=EB=B6=84=ED=95=A0=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4(Split=20Screen)=20=EB=B0=8F=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=A0=84=EA=B2=A9=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TenantFineGrainedPermissionsPage.tsx | 346 ++++++++++++------ 1 file changed, 234 insertions(+), 112 deletions(-) diff --git a/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsPage.tsx b/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsPage.tsx index 7542716f..1a1c48b3 100644 --- a/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsPage.tsx @@ -12,7 +12,24 @@ import { } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; import { TenantFineGrainedPermissionsTab } from "./TenantFineGrainedPermissionsTab"; -import { ShieldCheck, Search, Plus, UserPlus, Trash2, Settings, Shield } from "lucide-react"; +import { + ShieldCheck, + Search, + Plus, + UserPlus, + Trash2, + Settings, + Shield, + LayoutDashboard, + Building2, + Network, + Database, + Users, + KeyRound, + Key, + NotebookTabs, + Share2, +} from "lucide-react"; import { Badge } from "../../../components/ui/badge"; import { Button } from "../../../components/ui/button"; import { @@ -30,14 +47,10 @@ import { DialogTitle, } from "../../../components/ui/dialog"; import { Input } from "../../../components/ui/input"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "../../../components/ui/table"; +import { Avatar, AvatarFallback } from "../../../components/ui/avatar"; +import { ScrollArea } from "../../../components/ui/scroll-area"; +import { Separator } from "../../../components/ui/separator"; +import { Switch } from "../../../components/ui/switch"; import { toast } from "../../../components/ui/use-toast"; export function TenantFineGrainedPermissionsPage() { @@ -46,6 +59,8 @@ export function TenantFineGrainedPermissionsPage() { const [selectedTenantId, setSelectedTenantId] = useState(""); const [searchTerm, setSearchTerm] = useState(""); const [isDialogOpen, setIsDialogOpen] = useState(false); + const [activeUserId, setActiveUserId] = useState(null); + const [userSearchTerm, setUserSearchTerm] = useState(""); const { data: profile } = useQuery({ queryKey: ["me"], @@ -165,6 +180,9 @@ export function TenantFineGrainedPermissionsPage() { for (const rel of userRelations) { await removeSystemRelationMutation.mutateAsync({ userId, relation: rel }); } + if (activeUserId === userId) { + setActiveUserId(null); + } }; const usersQuery = useQuery({ @@ -175,26 +193,56 @@ export function TenantFineGrainedPermissionsPage() { const handleAddSystemUser = (userId: string) => { addSystemRelationMutation.mutate({ userId, relation: "overview_viewers" }); + setActiveUserId(userId); setIsDialogOpen(false); setSearchTerm(""); }; const searchResults = usersQuery.data?.items || []; - const systemMenus = [ - { label: t("ui.admin.nav.overview", "개요"), relation: "overview_viewers" }, - { label: t("ui.admin.nav.tenants", "테넌트"), relation: "tenants_viewers" }, - { label: t("ui.admin.nav.org_chart", "조직도"), relation: "org_chart_viewers" }, - { label: t("ui.admin.nav.worksmobile", "Worksmobile"), relation: "worksmobile_viewers" }, - { label: t("ui.admin.nav.ory_ssot", "Ory SSOT"), relation: "ory_ssot_viewers" }, - { label: t("ui.admin.nav.data_integrity", "정합성"), relation: "data_integrity_viewers" }, - { label: t("ui.admin.nav.users", "사용자"), relation: "users_viewers" }, - { label: t("ui.admin.nav.permissions_direct", "권한 부여"), relation: "permissions_direct_viewers" }, - { label: t("ui.admin.nav.auth_guard", "인증 가드"), relation: "auth_guard_viewers" }, - { label: t("ui.admin.nav.api_keys", "API 키"), relation: "api_keys_viewers" }, - { label: t("ui.admin.nav.audit_logs", "감사 로그"), relation: "audit_logs_viewers" }, + // Categorized system menus with descriptions and icons + const systemMenuCategories = [ + { + title: t("ui.admin.permissions_direct.cat_dashboard", "핵심 대시보드 및 분석"), + menus: [ + { label: t("ui.admin.nav.overview", "개요"), relation: "overview_viewers", desc: t("msg.admin.permissions_direct.desc_overview", "바론 전체 사양 및 시스템 상태 개요 정보"), icon: LayoutDashboard }, + { label: t("ui.admin.nav.audit_logs", "감사 로그"), relation: "audit_logs_viewers", desc: t("msg.admin.permissions_direct.desc_audit_logs", "시스템 전역 보안 감사 및 접속 이력 로그"), icon: NotebookTabs }, + ] + }, + { + title: t("ui.admin.permissions_direct.cat_resources", "핵심 리소스 관리"), + menus: [ + { label: t("ui.admin.nav.tenants", "테넌트"), relation: "tenants_viewers", desc: t("msg.admin.permissions_direct.desc_tenants", "고객 테넌트 목록, 신규 부모-자식 테넌트 관리"), icon: Building2 }, + { label: t("ui.admin.nav.org_chart", "조직도"), relation: "org_chart_viewers", desc: t("msg.admin.permissions_direct.desc_org_chart", "조직도 가시화 및 트리 배치 확인"), icon: Network }, + { label: t("ui.admin.nav.users", "사용자"), relation: "users_viewers", desc: t("msg.admin.permissions_direct.desc_users", "가입 사용자 목록, 승인 및 커스텀 클레임 수동 주입"), icon: Users }, + ] + }, + { + title: t("ui.admin.permissions_direct.cat_integrations", "인프라 연동 및 보안"), + menus: [ + { label: t("ui.admin.nav.worksmobile", "WORKS 연동"), relation: "worksmobile_viewers", desc: t("msg.admin.permissions_direct.desc_worksmobile", "라인웍스 연동 및 사내 임직원 패스워드 강제 동기화"), icon: Share2 }, + { label: t("ui.admin.nav.api_keys", "API 키"), relation: "api_keys_viewers", desc: t("msg.admin.permissions_direct.desc_api_keys", "조직도 연동을 위한 전역 서드파티 토큰 관리"), icon: Key }, + ] + }, + { + title: t("ui.admin.permissions_direct.cat_system", "아이덴티티 및 게이트 관리"), + menus: [ + { label: t("ui.admin.nav.ory_ssot", "Ory SSOT 시스템"), relation: "ory_ssot_viewers", desc: t("msg.admin.permissions_direct.desc_ory_ssot", "Redis 아이덴티티 미러 캐시 및 PostgreSQL read model 정합성 갱신"), icon: Database }, + { label: t("ui.admin.nav.data_integrity", "데이터 정합성"), relation: "data_integrity_viewers", desc: t("msg.admin.permissions_direct.desc_data_integrity", "고아 레코드 검출 및 DB 정합성 최종 검증기"), icon: ShieldCheck }, + { label: t("ui.admin.nav.auth_guard", "인증 가드"), relation: "auth_guard_viewers", desc: t("msg.admin.permissions_direct.desc_auth_guard", "정책엔진 기준으로 Keto ReBAC 관계 검증 시뮬레이터"), icon: KeyRound }, + { label: t("ui.admin.nav.permissions_direct", "권한 부여"), relation: "permissions_direct_viewers", desc: t("msg.admin.permissions_direct.desc_permissions_direct", "본 사이드바 메뉴 세부 권한 격자 및 테넌트 인가 설정 패널"), icon: Shield }, + ] + } ]; + const filteredRelations = systemRelations.filter( + (r) => + r.name.toLowerCase().includes(userSearchTerm.toLowerCase()) || + r.email.toLowerCase().includes(userSearchTerm.toLowerCase()), + ); + + const selectedUser = systemRelations.find((r) => r.userId === activeUserId); + return (
@@ -277,99 +325,173 @@ export function TenantFineGrainedPermissionsPage() { )} ) : ( - /* 시스템 메뉴 권한 (Admin Control) Tab Panel */ - - -
- - - {t("ui.admin.permissions_direct.tab_system_title", "글로벌 메뉴 접근 제어 (Admin Control)")} - - - {t( - "msg.admin.permissions_direct.tab_system_desc", - "사이드바 각 메뉴별 접근 권한을 사용자에게 직접 부여합니다. 최고 관리자(super_admin)는 기본적으로 언제나 모든 권한을 우회 통과합니다.", + /* 시스템 메뉴 권한 (Admin Control) Split Screen Panel */ +
+ {/* Left Panel: User List */} +
+
+
+

+ {t("ui.admin.permissions_direct.user_list", "대상 사용자")} ({filteredRelations.length}) +

+ +
+
+ + setUserSearchTerm(e.target.value)} + className="pl-8 h-8 text-xs" + /> +
+
+ +
+ {filteredRelations.length === 0 ? ( +
+ {t("msg.admin.permissions_direct.no_users_found", "등록된 사용자가 없습니다.")} +
+ ) : ( + filteredRelations.map((user) => { + const isSelected = activeUserId === user.userId; + const activeCount = user.relations.length; + + return ( + + ); + }) )} - -
- - - -
- - - - {t("ui.common.name", "이름")} - {systemMenus.map((menu) => ( - - {menu.label} - - ))} - {t("ui.common.action", "작업")} - - - - {systemRelations.length === 0 ? ( - - - {t("msg.admin.permissions_direct.system_empty", "지정된 글로벌 메뉴 세부 권한자가 없습니다. 사용자를 추가해 관리해 주세요.")} - - - ) : ( - systemRelations.map((user) => { - return ( - - -
- {user.name} - {user.email} -
-
- {systemMenus.map((menu) => { - const hasAccess = user.relations.includes(menu.relation); - return ( - - - - ); - })} - - - -
- ); - }) - )} -
-
-
-
- +
+
+ +
+
+ {menu.label} + + {menu.desc} + +
+
+ + handleSystemRelationChange(selectedUser.userId, menu.relation, val) + } + /> +
+ ); + })} + + +
+ ))} +
+ + + ) : ( +
+ +
+

+ {t("ui.admin.permissions_direct.no_user_selected", "사용자가 선택되지 않았습니다.")} +

+

+ {t("msg.admin.permissions_direct.no_user_selected_desc", "왼쪽의 사용자 리스트에서 권한을 변경할 인원을 선택해 주세요.")} +

+
+
+ )} +
+
)} {/* User Search Dialog for System relations */}