1
0
forked from baron/baron-sso

feat(adminfront): implement user role management and cleanup tenant list UI

- Add user role management (view, edit, bulk) in UserListPage, UserDetailPage, and UserCreatePage.
- Restrict role modification to super_admin only.
- Remove redundant action columns from tenant-related lists (TenantListPage, TenantSubTenantsPage, TenantUsersPage, TenantAdminsAndOwnersTab).
- Improve navigation by making table rows clickable where actions were removed.
This commit is contained in:
2026-05-13 14:50:11 +09:00
parent 5f48a1c172
commit a31eceaf16
7 changed files with 170 additions and 174 deletions

View File

@@ -152,6 +152,7 @@ function UserCreatePage() {
grade: "",
position: "",
jobTitle: "",
role: "user",
metadata: {},
},
});
@@ -366,6 +367,7 @@ function UserCreatePage() {
password: data.password,
name: data.name,
phone: data.phone,
role: data.role,
metadata,
};
@@ -644,6 +646,37 @@ function UserCreatePage() {
</div>
</div>
<div className="space-y-2">
<Label htmlFor="role">
{t("ui.admin.users.create.form.role", "역할")}
</Label>
<select
id="role"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
{...register("role")}
disabled={profile?.role !== "super_admin"}
>
<option value="super_admin">
{t("ui.admin.role.super_admin", "시스템 관리자")}
</option>
<option value="tenant_admin">
{t("ui.admin.role.tenant_admin", "테넌트 관리자")}
</option>
<option value="rp_admin">
{t("ui.admin.role.rp_admin", "서비스 관리자")}
</option>
<option value="user">
{t("ui.admin.role.user", "일반 사용자")}
</option>
</select>
<p className="text-xs text-muted-foreground">
{t(
"msg.admin.users.create.form.role_help",
"시스템 접근 권한을 결정합니다.",
)}
</p>
</div>
<Tabs value={userType} onValueChange={handleUserTypeChange}>
<TabsList className="flex h-auto w-full justify-start rounded-none border-b bg-transparent p-0 text-foreground">
<TabsTrigger

View File

@@ -743,10 +743,8 @@ function UserDetailPage() {
userType,
};
const profileData = { ...data };
profileData.role = undefined;
const payload: UserUpdateRequest = {
...profileData,
...data,
metadata,
};
@@ -1068,6 +1066,35 @@ function UserDetailPage() {
</span>
</div>
</div>
<div className="space-y-2">
<Label
htmlFor="role"
className="text-xs font-bold uppercase text-muted-foreground"
>
{t("ui.admin.users.detail.form.role", "역할")}
</Label>
<div className="flex h-11 items-center gap-3">
<select
id="role"
className="flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:opacity-50"
{...register("role")}
disabled={profile?.role !== "super_admin" || profile?.id === user.id}
>
<option value="super_admin">
{t("ui.admin.role.super_admin", "시스템 관리자")}
</option>
<option value="tenant_admin">
{t("ui.admin.role.tenant_admin", "테넌트 관리자")}
</option>
<option value="rp_admin">
{t("ui.admin.role.rp_admin", "서비스 관리자")}
</option>
<option value="user">
{t("ui.admin.role.user", "일반 사용자")}
</option>
</select>
</div>
</div>
</div>
<Tabs

View File

@@ -318,6 +318,11 @@ function UserListPage() {
bulkUpdateMutation.mutate({ userIds: selectedUserIds, status });
};
const handleBulkRoleChange = (role: string) => {
if (selectedUserIds.length === 0) return;
bulkUpdateMutation.mutate({ userIds: selectedUserIds, role });
};
const handleBulkDelete = () => {
if (selectedUserIds.length === 0) return;
if (
@@ -567,6 +572,15 @@ function UserListPage() {
{getSortIcon("status")}
</div>
</TableHead>
<TableHead
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("role")}
>
<div className="flex items-center">
{t("ui.admin.users.list.table.role", "ROLE")}
{getSortIcon("role")}
</div>
</TableHead>
<TableHead
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("tenant_dept")}
@@ -711,6 +725,48 @@ function UserListPage() {
</span>
</div>
</TableCell>
<TableCell>
<Select
defaultValue={user.role}
onValueChange={(value) =>
bulkUpdateMutation.mutate({
userIds: [user.id],
role: value,
})
}
disabled={
bulkUpdateMutation.isPending ||
profile?.role !== "super_admin" ||
user.id === profile?.id
}
>
<SelectTrigger className="h-8 w-[140px] border-none bg-transparent hover:bg-muted/50 transition-colors px-0 font-medium">
<SelectValue />
</SelectTrigger>
<SelectContent>
{profile?.role === "super_admin" && (
<SelectItem value="super_admin">
{t(
"ui.admin.role.super_admin",
"시스템 관리자",
)}
</SelectItem>
)}
<SelectItem value="tenant_admin">
{t(
"ui.admin.role.tenant_admin",
"테넌트 관리자",
)}
</SelectItem>
<SelectItem value="rp_admin">
{t("ui.admin.role.rp_admin", "서비스 관리자")}
</SelectItem>
<SelectItem value="user">
{t("ui.admin.role.user", "일반 사용자")}
</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<div className="flex flex-col gap-1">
<span className="text-sm font-medium">
@@ -775,6 +831,34 @@ function UserListPage() {
{t("ui.common.status.inactive", "비활성화")}
</Button>
<div className="w-px h-4 bg-background/20 mx-1" />
{profile?.role === "super_admin" && (
<>
<Select onValueChange={handleBulkRoleChange}>
<SelectTrigger className="h-8 w-[140px] bg-transparent border-background/20 text-background text-xs">
<SelectValue
placeholder={
t("ui.admin.users.list.table.role", "ROLE")
}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="super_admin">
{t("ui.admin.role.super_admin", "시스템 관리자")}
</SelectItem>
<SelectItem value="tenant_admin">
{t("ui.admin.role.tenant_admin", "테넌트 관리자")}
</SelectItem>
<SelectItem value="rp_admin">
{t("ui.admin.role.rp_admin", "서비스 관리자")}
</SelectItem>
<SelectItem value="user">
{t("ui.admin.role.user", "일반 사용자")}
</SelectItem>
</SelectContent>
</Select>
<div className="w-px h-4 bg-background/20 mx-1" />
</>
)}
<Button
variant="ghost"
size="sm"