From 919bcd27e8346d6eae4656774362a3f5d8b7b576 Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 20 Feb 2026 17:56:53 +0900 Subject: [PATCH] =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C?= =?UTF-8?q?=20UI/UX=EB=A5=BC=20=EC=A0=84=EB=A9=B4=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminfront/src/app/routes.tsx | 6 +- .../src/components/layout/AppLayout.tsx | 5 - .../src/components/layout/RoleSwitcher.tsx | 93 +++-- .../tenants/routes/TenantAdminsTab.tsx | 315 +++++++++------- .../tenants/routes/TenantCreatePage.tsx | 34 +- .../tenants/routes/TenantDetailPage.tsx | 68 ++-- .../tenants/routes/TenantListPage.tsx | 8 + .../tenants/routes/TenantProfilePage.tsx | 65 ++-- .../tenants/routes/TenantSchemaPage.tsx | 74 ++-- .../routes/GlobalUserGroupListPage.tsx | 141 -------- .../routes/TenantUserGroupsTab.tsx | 340 +++++++++++------- .../routes/UserGroupDetailPage.tsx | 311 ++++++++-------- .../src/features/users/UserCreatePage.tsx | 34 ++ .../src/features/users/UserDetailPage.tsx | 36 ++ .../src/features/users/UserListPage.tsx | 11 + adminfront/src/lib/adminApi.ts | 46 ++- adminfront/src/locales/en.toml | 2 +- adminfront/src/locales/ko.toml | 239 +++++++++--- 18 files changed, 1092 insertions(+), 736 deletions(-) delete mode 100644 adminfront/src/features/user-groups/routes/GlobalUserGroupListPage.tsx diff --git a/adminfront/src/app/routes.tsx b/adminfront/src/app/routes.tsx index 093a92a9..a3a8579e 100644 --- a/adminfront/src/app/routes.tsx +++ b/adminfront/src/app/routes.tsx @@ -14,7 +14,6 @@ import TenantDetailPage from "../features/tenants/routes/TenantDetailPage"; import TenantListPage from "../features/tenants/routes/TenantListPage"; import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage"; import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage"; -import GlobalUserGroupListPage from "../features/user-groups/routes/GlobalUserGroupListPage"; import { TenantUserGroupsTab } from "../features/user-groups/routes/TenantUserGroupsTab"; import { UserGroupDetailPage } from "../features/user-groups/routes/UserGroupDetailPage"; import UserCreatePage from "../features/users/UserCreatePage"; @@ -42,7 +41,6 @@ export const router = createBrowserRouter( { path: "users", element: }, { path: "users/new", element: }, { path: "users/:id", element: }, - { path: "user-groups", element: }, { path: "tenants", element: }, { path: "tenants/new", element: }, { @@ -51,12 +49,12 @@ export const router = createBrowserRouter( children: [ { index: true, element: }, { path: "admins", element: }, - { path: "user-groups", element: }, + { path: "organization", element: }, { path: "schema", element: }, ], }, { - path: "tenants/:tenantId/user-groups/:id", + path: "tenants/:tenantId/organization/:id", element: , }, { path: "api-keys", element: }, diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx index 6fda7c27..fcec20bd 100644 --- a/adminfront/src/components/layout/AppLayout.tsx +++ b/adminfront/src/components/layout/AppLayout.tsx @@ -30,11 +30,6 @@ const navItems = [ to: "/tenants", icon: Building2, }, - { - label: "ui.admin.nav.user_groups", - to: "/user-groups", - icon: Users, - }, { label: "ui.admin.nav.users", to: "/users", icon: Users }, { label: "ui.admin.nav.api_keys", to: "/api-keys", icon: Key }, { label: "ui.admin.nav.audit_logs", to: "/audit-logs", icon: NotebookTabs }, diff --git a/adminfront/src/components/layout/RoleSwitcher.tsx b/adminfront/src/components/layout/RoleSwitcher.tsx index ea4cb2c8..145328d5 100644 --- a/adminfront/src/components/layout/RoleSwitcher.tsx +++ b/adminfront/src/components/layout/RoleSwitcher.tsx @@ -1,9 +1,13 @@ +import { ChevronDown, ChevronUp, Wrench } from "lucide-react"; import type { FC } from "react"; import { useEffect, useState } from "react"; import { t } from "../../lib/i18n"; const RoleSwitcher: FC = () => { const [currentRole, setCurrentRole] = useState("super_admin"); + const [isCollapsed, setIsCollapsed] = useState(() => { + return window.localStorage.getItem("RoleSwitcher-Collapsed") === "true"; + }); useEffect(() => { // localStorage에서 역할 읽기 @@ -16,6 +20,12 @@ const RoleSwitcher: FC = () => { } }, []); + const toggleCollapse = () => { + const nextState = !isCollapsed; + setIsCollapsed(nextState); + window.localStorage.setItem("RoleSwitcher-Collapsed", String(nextState)); + }; + const switchRole = (role: string) => { // localStorage 설정 window.localStorage.setItem("X-Mock-Role", role); @@ -42,47 +52,80 @@ const RoleSwitcher: FC = () => { zIndex: 9999, background: "#1A1F2C", color: "white", - padding: "10px", + padding: "8px 12px", borderRadius: "8px", boxShadow: "0 4px 12px rgba(0,0,0,0.3)", display: "flex", flexDirection: "column", - gap: "8px", + gap: isCollapsed ? "0" : "8px", fontSize: "12px", + transition: "all 0.3s ease", + border: "1px solid #333", }} >
- {t("ui.admin.dev_role_switcher", "🛠 DEV Role Switcher")} +
+ + {!isCollapsed && ( + {t("ui.admin.dev_role_switcher", "DEV Role Switcher")} + )} + {isCollapsed && ( + + {currentRole.toUpperCase()} + + )} +
+ {isCollapsed ? : }
- {( - ["super_admin", "tenant_admin", "rp_admin", "tenant_member"] as const - ).map((role) => ( - - ))} + {( + ["super_admin", "tenant_admin", "rp_admin", "tenant_member"] as const + ).map((role) => ( + + ))} + + )} ); }; diff --git a/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx b/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx index e10979b8..6081f52d 100644 --- a/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx +++ b/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx @@ -1,7 +1,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Plus, Search, ShieldCheck, Trash2, UserPlus } from "lucide-react"; +import type { AxiosError } from "axios"; +import { Plus, Search, ShieldCheck, Trash2, UserPlus, Users } from "lucide-react"; import { useState } from "react"; import { useParams } from "react-router-dom"; +import { toast } from "sonner"; +import { Badge } from "../../../components/ui/badge"; import { Button } from "../../../components/ui/button"; import { Card, @@ -10,6 +13,14 @@ import { CardHeader, CardTitle, } from "../../../components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../../../components/ui/dialog"; import { Input } from "../../../components/ui/input"; import { Table, @@ -25,40 +36,50 @@ import { fetchUsers, removeTenantAdmin, } from "../../../lib/adminApi"; +import { t } from "../../../lib/i18n"; -function TenantAdminsTab() { +export function TenantAdminsTab() { const { tenantId } = useParams<{ tenantId: string }>(); const queryClient = useQueryClient(); const [searchTerm, setSearchTerm] = useState(""); + const [isDialogOpen, setIsAddDialogOpen] = useState(false); if (!tenantId) return null; - // 현재 관리자 목록 + // 현재 관리자 목록 조회 const adminsQuery = useQuery({ queryKey: ["tenant-admins", tenantId], queryFn: () => fetchTenantAdmins(tenantId), enabled: !!tenantId, }); - // 전체 사용자 목록 (관리자 추가용) + // 사용자 검색 조회 (2자 이상 입력 시) const usersQuery = useQuery({ - queryKey: ["users", { limit: 100, search: searchTerm }], - queryFn: () => fetchUsers(100, 0, searchTerm), - enabled: searchTerm.length > 1, + queryKey: ["admin-users-search", searchTerm], + queryFn: () => fetchUsers(20, 0, searchTerm), + enabled: isDialogOpen && searchTerm.length >= 2, }); const addMutation = useMutation({ mutationFn: (userId: string) => addTenantAdmin(tenantId, userId), onSuccess: () => { - adminsQuery.refetch(); + queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] }); + toast.success(t("msg.admin.tenants.admins.add_success", "관리자가 추가되었습니다.")); setSearchTerm(""); }, + onError: (err: AxiosError<{ error?: string }>) => { + toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다.")); + }, }); const removeMutation = useMutation({ mutationFn: (userId: string) => removeTenantAdmin(tenantId, userId), onSuccess: () => { - adminsQuery.refetch(); + queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] }); + toast.success(t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다.")); + }, + onError: (err: AxiosError<{ error?: string }>) => { + toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다.")); }, }); @@ -67,144 +88,176 @@ function TenantAdminsTab() { }; const handleRemoveAdmin = (userId: string, userName: string) => { - if (window.confirm(`${userName} 사용자의 관리자 권한을 회수할까요?`)) { + if (window.confirm(t("msg.admin.tenants.admins.remove_confirm", { name: userName }))) { removeMutation.mutate(userId); } }; + const currentAdmins = adminsQuery.data || []; + const searchResults = usersQuery.data?.items || []; + return ( -
- {/* 현재 테넌트 관리자 */} - - - - - 테넌트 관리자 - - - 이 테넌트의 자원을 관리할 수 있는 권한을 가진 사용자들입니다. - - - - - - - 이름 - 이메일 - 회수 - - - - {adminsQuery.data?.length === 0 && ( - - - 등록된 관리자가 없습니다. - - - )} - {adminsQuery.data?.map((admin) => ( - - - {admin.name || "Unknown"} - - {admin.email} - - - - - ))} - -
-
-
- - {/* 사용자 검색 및 추가 */} - - -
- - - 관리자 추가 +
+ + +
+ + + {t("ui.admin.tenants.admins.title", "테넌트 관리자")} -
- - 관리자로 추가할 사용자를 검색하세요 (이름 또는 이메일). - -
- -
- - setSearchTerm(e.target.value)} - /> + + {t("msg.admin.tenants.admins.subtitle", "이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다.")} +
- - - - 사용자 - 추가 - - - - {searchTerm.length < 2 && ( + { + setIsAddDialogOpen(open); + if (!open) setSearchTerm(""); + }}> + + + + + + + {t("ui.admin.tenants.admins.dialog_title", "새 관리자 추가")} + + + {t("ui.admin.tenants.admins.dialog_description", "이름 또는 이메일로 사용자를 검색하세요.")} + + + +
+
+ + setSearchTerm(e.target.value)} + /> +
+ +
+ {searchTerm.length < 2 ? ( +
+ +

{t("ui.admin.tenants.admins.dialog_search_hint", "검색어를 입력해 주세요.")}

+
+ ) : usersQuery.isLoading ? ( +
+
+
+ ) : searchResults.length === 0 ? ( +
+ {t("ui.admin.tenants.admins.dialog_no_results", "검색 결과가 없습니다.")} +
+ ) : ( +
+ {searchResults.map((user) => { + const isAlreadyAdmin = currentAdmins.some((a) => a.id === user.id); + return ( +
+
+
+ {user.name.charAt(0)} +
+
+ {user.name} + {user.email} +
+
+ +
+ ); + })} +
+ )} +
+
+
+
+ + + +
+
+ - - 사용자 이름을 입력하여 검색하세요. - + + {t("ui.admin.tenants.admins.table_name", "이름")} + + + {t("ui.admin.tenants.admins.table_email", "이메일")} + + + {t("ui.admin.tenants.admins.table_actions", "액션")} + - )} - {searchTerm.length >= 2 && - usersQuery.data?.items.length === 0 && ( + + + {adminsQuery.isLoading ? ( - - 검색 결과가 없습니다. + +
- )} - {usersQuery.data?.items - .filter((u) => !adminsQuery.data?.some((a) => a.id === u.id)) - .map((user) => ( - - -
{user.name}
-
- {user.email} + ) : currentAdmins.length === 0 ? ( + + +
+ +

{t("msg.admin.tenants.admins.empty", "등록된 관리자가 없습니다.")}

- - -
- ))} - -
+ ) : ( + currentAdmins.map((admin) => ( + + +
+
+ {admin.name.charAt(0)} +
+ {admin.name} +
+
+ + {admin.email} + + + + +
+ )) + )} + + +
diff --git a/adminfront/src/features/tenants/routes/TenantCreatePage.tsx b/adminfront/src/features/tenants/routes/TenantCreatePage.tsx index b5af1921..0015ca5f 100644 --- a/adminfront/src/features/tenants/routes/TenantCreatePage.tsx +++ b/adminfront/src/features/tenants/routes/TenantCreatePage.tsx @@ -21,6 +21,7 @@ import { t } from "../../../lib/i18n"; function TenantCreatePage() { const navigate = useNavigate(); const [name, setName] = useState(""); + const [type, setType] = useState("COMPANY"); const [slug, setSlug] = useState(""); const [description, setDescription] = useState(""); const [status, setStatus] = useState("active"); @@ -30,6 +31,7 @@ function TenantCreatePage() { mutationFn: () => createTenant({ name, + type, slug: slug || undefined, description: description || undefined, status, @@ -92,14 +94,30 @@ function TenantCreatePage() {
setName(e.target.value)} />
+ +
+
+