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:
@@ -371,22 +371,19 @@ export function TenantAdminsAndOwnersTab() {
|
|||||||
<TableHead className="font-bold">
|
<TableHead className="font-bold">
|
||||||
{t("ui.admin.tenants.owners.table_email", "이메일")}
|
{t("ui.admin.tenants.owners.table_email", "이메일")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right font-bold w-[100px]">
|
|
||||||
{t("ui.admin.tenants.owners.table_actions", "액션")}
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{ownersQuery.isLoading ? (
|
{ownersQuery.isLoading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="h-32 text-center">
|
<TableCell colSpan={2} className="h-32 text-center">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : currentOwners.length === 0 ? (
|
) : currentOwners.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={3}
|
colSpan={2}
|
||||||
className="h-32 text-center text-muted-foreground"
|
className="h-32 text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
@@ -404,7 +401,8 @@ export function TenantAdminsAndOwnersTab() {
|
|||||||
currentOwners.map((owner) => (
|
currentOwners.map((owner) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={owner.id}
|
key={owner.id}
|
||||||
className="hover:bg-muted/30 transition-colors group"
|
className="hover:bg-muted/30 transition-colors group cursor-pointer"
|
||||||
|
onClick={() => navigate(`/users/${owner.id}`)}
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -417,46 +415,6 @@ export function TenantAdminsAndOwnersTab() {
|
|||||||
<TableCell className="text-muted-foreground italic">
|
<TableCell className="text-muted-foreground italic">
|
||||||
{owner.email}
|
{owner.email}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
|
||||||
<span className="relative inline-block group/tt">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={`opacity-0 group-hover:opacity-100 transition-all ${
|
|
||||||
owner.id === currentUserId ||
|
|
||||||
currentOwners.length <= 1
|
|
||||||
? "opacity-50 cursor-not-allowed pointer-events-none"
|
|
||||||
: "text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
||||||
}`}
|
|
||||||
onClick={() =>
|
|
||||||
handleRemoveOwner(owner.id, owner.name)
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
removeOwnerMutation.isPending ||
|
|
||||||
owner.id === currentUserId ||
|
|
||||||
currentOwners.length <= 1
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<span className="pointer-events-none absolute bottom-full right-0 z-[100] mb-2 w-max rounded bg-foreground px-2 py-1 text-xs text-background opacity-0 shadow-lg transition-opacity group-hover/tt:opacity-100">
|
|
||||||
{owner.id === currentUserId
|
|
||||||
? t(
|
|
||||||
"msg.admin.tenants.owners.remove_self",
|
|
||||||
"본인의 권한은 회수할 수 없습니다.",
|
|
||||||
)
|
|
||||||
: currentOwners.length <= 1
|
|
||||||
? t(
|
|
||||||
"msg.admin.tenants.owners.remove_last",
|
|
||||||
"마지막 소유자는 회수할 수 없습니다.",
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"ui.admin.tenants.owners.remove_title",
|
|
||||||
"소유자 권한 회수",
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -502,22 +460,19 @@ export function TenantAdminsAndOwnersTab() {
|
|||||||
<TableHead className="font-bold">
|
<TableHead className="font-bold">
|
||||||
{t("ui.admin.tenants.admins.table_email", "이메일")}
|
{t("ui.admin.tenants.admins.table_email", "이메일")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right font-bold w-[100px]">
|
|
||||||
{t("ui.admin.tenants.admins.table_actions", "액션")}
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{adminsQuery.isLoading ? (
|
{adminsQuery.isLoading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="h-32 text-center">
|
<TableCell colSpan={2} className="h-32 text-center">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : currentAdmins.length === 0 ? (
|
) : currentAdmins.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={3}
|
colSpan={2}
|
||||||
className="h-32 text-center text-muted-foreground"
|
className="h-32 text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
@@ -535,7 +490,8 @@ export function TenantAdminsAndOwnersTab() {
|
|||||||
currentAdmins.map((admin) => (
|
currentAdmins.map((admin) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={admin.id}
|
key={admin.id}
|
||||||
className="hover:bg-muted/30 transition-colors group"
|
className="hover:bg-muted/30 transition-colors group cursor-pointer"
|
||||||
|
onClick={() => navigate(`/users/${admin.id}`)}
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -548,46 +504,6 @@ export function TenantAdminsAndOwnersTab() {
|
|||||||
<TableCell className="text-muted-foreground italic">
|
<TableCell className="text-muted-foreground italic">
|
||||||
{admin.email}
|
{admin.email}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
|
||||||
<span className="relative inline-block group/tt">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={`opacity-0 group-hover:opacity-100 transition-all ${
|
|
||||||
admin.id === currentUserId ||
|
|
||||||
currentAdmins.length <= 1
|
|
||||||
? "opacity-50 cursor-not-allowed pointer-events-none"
|
|
||||||
: "text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
||||||
}`}
|
|
||||||
onClick={() =>
|
|
||||||
handleRemoveAdmin(admin.id, admin.name)
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
removeAdminMutation.isPending ||
|
|
||||||
admin.id === currentUserId ||
|
|
||||||
currentAdmins.length <= 1
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<span className="pointer-events-none absolute bottom-full right-0 z-[100] mb-2 w-max rounded bg-foreground px-2 py-1 text-xs text-background opacity-0 shadow-lg transition-opacity group-hover/tt:opacity-100">
|
|
||||||
{admin.id === currentUserId
|
|
||||||
? t(
|
|
||||||
"msg.admin.tenants.admins.remove_self",
|
|
||||||
"본인의 권한은 회수할 수 없습니다.",
|
|
||||||
)
|
|
||||||
: currentAdmins.length <= 1
|
|
||||||
? t(
|
|
||||||
"msg.admin.tenants.admins.remove_last",
|
|
||||||
"마지막 관리자는 회수할 수 없습니다.",
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"ui.admin.tenants.admins.remove_title",
|
|
||||||
"관리자 권한 회수",
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -850,15 +850,12 @@ function TenantListPage() {
|
|||||||
{getSortIcon("updatedAt")}
|
{getSortIcon("updatedAt")}
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="whitespace-nowrap">
|
|
||||||
{t("ui.common.actions", "액션")}
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{query.isLoading && (
|
{query.isLoading && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={9}>
|
<TableCell colSpan={8}>
|
||||||
{t("msg.common.loading", "로딩 중...")}
|
{t("msg.common.loading", "로딩 중...")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -866,7 +863,7 @@ function TenantListPage() {
|
|||||||
{!query.isLoading && tenants.length === 0 && (
|
{!query.isLoading && tenants.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={9}
|
colSpan={8}
|
||||||
className="text-center py-8 text-muted-foreground"
|
className="text-center py-8 text-muted-foreground"
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
@@ -954,22 +951,6 @@ function TenantListPage() {
|
|||||||
)
|
)
|
||||||
: "-"}
|
: "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
disabled={
|
|
||||||
isSeedTenant(tenant) || deleteMutation.isPending
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
handleDelete(tenant.id, tenant.name)
|
|
||||||
}
|
|
||||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
||||||
>
|
|
||||||
<Trash2 size={14} className="mr-2" />
|
|
||||||
{t("ui.common.delete", "삭제")}
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -72,16 +72,13 @@ function TenantSubTenantsPage() {
|
|||||||
<TableHead>
|
<TableHead>
|
||||||
{t("ui.admin.tenants.sub.table.status", "STATUS")}
|
{t("ui.admin.tenants.sub.table.status", "STATUS")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right">
|
|
||||||
{t("ui.admin.tenants.sub.table.action", "ACTION")}
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{subTenants.length === 0 && (
|
{subTenants.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={4}
|
colSpan={3}
|
||||||
className="text-center py-8 text-muted-foreground"
|
className="text-center py-8 text-muted-foreground"
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
@@ -92,7 +89,11 @@ function TenantSubTenantsPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{subTenants.map((tenant) => (
|
{subTenants.map((tenant) => (
|
||||||
<TableRow key={tenant.id}>
|
<TableRow
|
||||||
|
key={tenant.id}
|
||||||
|
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||||
|
onClick={() => navigate(`/tenants/${tenant.id}`)}
|
||||||
|
>
|
||||||
<TableCell className="font-semibold">
|
<TableCell className="font-semibold">
|
||||||
{tenant.name}
|
{tenant.name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -108,16 +109,6 @@ function TenantSubTenantsPage() {
|
|||||||
{t(`ui.common.status.${tenant.status}`, tenant.status)}
|
{t(`ui.common.status.${tenant.status}`, tenant.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => navigate(`/tenants/${tenant.id}`)}
|
|
||||||
>
|
|
||||||
{t("ui.admin.tenants.sub.manage", "관리")}{" "}
|
|
||||||
<ArrowRight size={12} className="ml-1" />
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -137,15 +137,12 @@ function TenantUsersPage() {
|
|||||||
<TableHead>
|
<TableHead>
|
||||||
{t("ui.admin.tenants.members.table.status", "STATUS")}
|
{t("ui.admin.tenants.members.table.status", "STATUS")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[80px] text-right">
|
|
||||||
{t("ui.admin.tenants.members.table.actions", "ACTIONS")}
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{usersQuery.isLoading ? (
|
{usersQuery.isLoading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="text-center py-20">
|
<TableCell colSpan={4} className="text-center py-20">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Loader2
|
<Loader2
|
||||||
className="animate-spin text-muted-foreground"
|
className="animate-spin text-muted-foreground"
|
||||||
@@ -160,7 +157,7 @@ function TenantUsersPage() {
|
|||||||
) : users.length === 0 ? (
|
) : users.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={5}
|
colSpan={4}
|
||||||
className="text-center py-8 text-muted-foreground"
|
className="text-center py-8 text-muted-foreground"
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
@@ -171,7 +168,11 @@ function TenantUsersPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
users.map((user) => (
|
users.map((user) => (
|
||||||
<TableRow key={user.id}>
|
<TableRow
|
||||||
|
key={user.id}
|
||||||
|
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||||
|
onClick={() => navigate(`/users/${user.id}`)}
|
||||||
|
>
|
||||||
<TableCell className="font-semibold">
|
<TableCell className="font-semibold">
|
||||||
{user.name}
|
{user.name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -198,43 +199,6 @@ function TenantUsersPage() {
|
|||||||
{t(`ui.common.status.${user.status}`, user.status)}
|
{t(`ui.common.status.${user.status}`, user.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
>
|
|
||||||
<MoreHorizontal size={16} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link to={`/users/${user.id}`}>
|
|
||||||
<User size={14} className="mr-2" />
|
|
||||||
{t(
|
|
||||||
"ui.admin.tenants.members.view_profile",
|
|
||||||
"상세 정보",
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="text-destructive focus:text-destructive"
|
|
||||||
onClick={() =>
|
|
||||||
handleRemoveMember(user.id, user.name)
|
|
||||||
}
|
|
||||||
disabled={removeTenantMutation.isPending}
|
|
||||||
>
|
|
||||||
<UserMinus size={14} className="mr-2" />
|
|
||||||
{t(
|
|
||||||
"ui.admin.tenants.members.remove",
|
|
||||||
"조직에서 제외",
|
|
||||||
)}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ function UserCreatePage() {
|
|||||||
grade: "",
|
grade: "",
|
||||||
position: "",
|
position: "",
|
||||||
jobTitle: "",
|
jobTitle: "",
|
||||||
|
role: "user",
|
||||||
metadata: {},
|
metadata: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -366,6 +367,7 @@ function UserCreatePage() {
|
|||||||
password: data.password,
|
password: data.password,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
|
role: data.role,
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -644,6 +646,37 @@ function UserCreatePage() {
|
|||||||
</div>
|
</div>
|
||||||
</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}>
|
<Tabs value={userType} onValueChange={handleUserTypeChange}>
|
||||||
<TabsList className="flex h-auto w-full justify-start rounded-none border-b bg-transparent p-0 text-foreground">
|
<TabsList className="flex h-auto w-full justify-start rounded-none border-b bg-transparent p-0 text-foreground">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
|
|||||||
@@ -743,10 +743,8 @@ function UserDetailPage() {
|
|||||||
userType,
|
userType,
|
||||||
};
|
};
|
||||||
|
|
||||||
const profileData = { ...data };
|
|
||||||
profileData.role = undefined;
|
|
||||||
const payload: UserUpdateRequest = {
|
const payload: UserUpdateRequest = {
|
||||||
...profileData,
|
...data,
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1068,6 +1066,35 @@ function UserDetailPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -318,6 +318,11 @@ function UserListPage() {
|
|||||||
bulkUpdateMutation.mutate({ userIds: selectedUserIds, status });
|
bulkUpdateMutation.mutate({ userIds: selectedUserIds, status });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBulkRoleChange = (role: string) => {
|
||||||
|
if (selectedUserIds.length === 0) return;
|
||||||
|
bulkUpdateMutation.mutate({ userIds: selectedUserIds, role });
|
||||||
|
};
|
||||||
|
|
||||||
const handleBulkDelete = () => {
|
const handleBulkDelete = () => {
|
||||||
if (selectedUserIds.length === 0) return;
|
if (selectedUserIds.length === 0) return;
|
||||||
if (
|
if (
|
||||||
@@ -567,6 +572,15 @@ function UserListPage() {
|
|||||||
{getSortIcon("status")}
|
{getSortIcon("status")}
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</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
|
<TableHead
|
||||||
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
|
className="whitespace-nowrap cursor-pointer hover:bg-muted/50 transition-colors"
|
||||||
onClick={() => requestSort("tenant_dept")}
|
onClick={() => requestSort("tenant_dept")}
|
||||||
@@ -711,6 +725,48 @@ function UserListPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</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>
|
<TableCell>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
@@ -775,6 +831,34 @@ function UserListPage() {
|
|||||||
{t("ui.common.status.inactive", "비활성화")}
|
{t("ui.common.status.inactive", "비활성화")}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="w-px h-4 bg-background/20 mx-1" />
|
<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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user