forked from baron/baron-sso
SSOT 전환
This commit is contained in:
221
adminfront/src/features/tenants/routes/TenantGroupsPage.tsx
Normal file
221
adminfront/src/features/tenants/routes/TenantGroupsPage.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Plus, RefreshCw, Trash2, Users, UserPlus, UserMinus, Shield } 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 {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { Label } from "../../../components/ui/label";
|
||||
import { fetchGroups, createGroup, deleteGroup, fetchUsers, addGroupMember, removeGroupMember } from "../../../lib/adminApi";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
|
||||
function TenantGroupsPage() {
|
||||
const { tenantId } = useParams<{ tenantId: string }>();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [newGroupName, setNewGroupName] = useState("");
|
||||
const [newGroupDesc, setNewGroupNameDesc] = useState("");
|
||||
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
|
||||
|
||||
// 그룹 목록 조회
|
||||
const groupsQuery = useQuery({
|
||||
queryKey: ["groups", tenantId],
|
||||
queryFn: () => fetchGroups(tenantId!),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
|
||||
// 사용자 목록 조회 (멤버 추가용)
|
||||
const usersQuery = useQuery({
|
||||
queryKey: ["users", { limit: 100 }],
|
||||
queryFn: () => fetchUsers(100, 0),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createGroup(tenantId!, { name: newGroupName, description: newGroupDesc }),
|
||||
onSuccess: () => {
|
||||
groupsQuery.refetch();
|
||||
setNewGroupName("");
|
||||
setNewGroupNameDesc("");
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => deleteGroup(id),
|
||||
onSuccess: () => groupsQuery.refetch(),
|
||||
});
|
||||
|
||||
const addMemberMutation = useMutation({
|
||||
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => addGroupMember(groupId, userId),
|
||||
onSuccess: () => groupsQuery.refetch(),
|
||||
});
|
||||
|
||||
const removeMemberMutation = useMutation({
|
||||
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => removeGroupMember(groupId, userId),
|
||||
onSuccess: () => groupsQuery.refetch(),
|
||||
});
|
||||
|
||||
const handleAddMember = (groupId: string) => {
|
||||
const userId = window.prompt("추가할 사용자의 UUID를 입력하세요:");
|
||||
if (userId) {
|
||||
addMemberMutation.mutate({ groupId, userId });
|
||||
}
|
||||
};
|
||||
|
||||
const currentGroup = groupsQuery.data?.find(g => g.id === selectedGroupId);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 mt-6">
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{/* 그룹 생성 폼 */}
|
||||
<Card className="bg-[var(--color-panel)] md:col-span-1 border-primary/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<Plus size={16} /> 새 그룹 생성
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="name">그룹 이름</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newGroupName}
|
||||
onChange={e => setNewGroupName(e.target.value)}
|
||||
placeholder="예: 개발팀, 인사팀"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="desc">설명</Label>
|
||||
<Input
|
||||
id="desc"
|
||||
value={newGroupDesc}
|
||||
onChange={e => setNewGroupNameDesc(e.target.value)}
|
||||
placeholder="그룹 용도 설명"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => createMutation.mutate()}
|
||||
disabled={!newGroupName || createMutation.isPending}
|
||||
>
|
||||
생성하기
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 그룹 목록 */}
|
||||
<Card className="bg-[var(--color-panel)] md:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>User Groups</CardTitle>
|
||||
<CardDescription>이 테넌트에 정의된 사용자 그룹 목록입니다.</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => groupsQuery.refetch()}>
|
||||
<RefreshCw size={14} />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>NAME</TableHead>
|
||||
<TableHead>MEMBERS</TableHead>
|
||||
<TableHead className="text-right">ACTIONS</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupsQuery.data?.map((group) => (
|
||||
<TableRow
|
||||
key={group.id}
|
||||
className={`cursor-pointer ${selectedGroupId === group.id ? 'bg-primary/5' : ''}`}
|
||||
onClick={() => setSelectedGroupId(group.id)}
|
||||
>
|
||||
<TableCell>
|
||||
<div className="font-semibold flex items-center gap-2">
|
||||
<Users size={14} className="text-muted-foreground" />
|
||||
{group.name}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">{group.description}</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{group.members?.length || 0} 명</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); handleAddMember(group.id); }}>
|
||||
<UserPlus size={14} />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); deleteMutation.mutate(group.id); }}>
|
||||
<Trash2 size={14} className="text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 멤버 관리 섹션 (선택된 그룹이 있을 때) */}
|
||||
{currentGroup && (
|
||||
<Card className="bg-[var(--color-panel)] border-t-4 border-t-primary">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield size={18} className="text-primary" />
|
||||
[{currentGroup.name}] 멤버 관리
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead className="text-right">제거</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{currentGroup.members?.length === 0 && (
|
||||
<TableRow><TableCell colSpan={3} className="text-center py-4 text-muted-foreground">멤버가 없습니다.</TableCell></TableRow>
|
||||
)}
|
||||
{currentGroup.members?.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-medium">{user.name}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{user.email}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeMemberMutation.mutate({ groupId: currentGroup.id, userId: user.id })}
|
||||
>
|
||||
<UserMinus size={14} className="text-destructive" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TenantGroupsPage;
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Building2, Plus, ArrowRight } from "lucide-react";
|
||||
import { Link, useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../../components/ui/card";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { fetchTenants } from "../../../lib/adminApi";
|
||||
|
||||
function TenantSubTenantsPage() {
|
||||
const { tenantId } = useParams<{ tenantId: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["sub-tenants", tenantId],
|
||||
queryFn: () => fetchTenants(50, 0, tenantId),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
|
||||
const subTenants = data?.items ?? [];
|
||||
|
||||
return (
|
||||
<Card className="mt-6 bg-[var(--color-panel)]">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Building2 size={18} className="text-primary" />
|
||||
Sub-tenants ({subTenants.length})
|
||||
</CardTitle>
|
||||
<CardDescription>현재 테넌트 하위에 생성된 조직입니다.</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" asChild>
|
||||
<Link to={`/tenants/new?parentId=${tenantId}`}>
|
||||
<Plus size={14} className="mr-1" />
|
||||
하위 테넌트 추가
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>NAME</TableHead>
|
||||
<TableHead>SLUG</TableHead>
|
||||
<TableHead>STATUS</TableHead>
|
||||
<TableHead className="text-right">ACTION</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{subTenants.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground">
|
||||
하위 테넌트가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{subTenants.map((t) => (
|
||||
<TableRow key={t.id}>
|
||||
<TableCell className="font-semibold">{t.name}</TableCell>
|
||||
<TableCell className="text-xs font-mono">{t.slug}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={t.status === "active" ? "default" : "secondary"}>
|
||||
{t.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm" onClick={() => navigate(`/tenants/${t.id}`)}>
|
||||
관리 <ArrowRight size={12} className="ml-1" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default TenantSubTenantsPage;
|
||||
91
adminfront/src/features/tenants/routes/TenantUsersPage.tsx
Normal file
91
adminfront/src/features/tenants/routes/TenantUsersPage.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { User, Mail, Phone, ShieldCheck } from "lucide-react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../../../components/ui/card";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { fetchUsers, fetchTenant } from "../../../lib/adminApi";
|
||||
|
||||
function TenantUsersPage() {
|
||||
const { tenantId } = useParams<{ tenantId: string }>();
|
||||
|
||||
// 테넌트의 슬러그(companyCode)를 먼저 가져옴
|
||||
const tenantQuery = useQuery({
|
||||
queryKey: ["tenant", tenantId],
|
||||
queryFn: () => fetchTenant(tenantId!),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
|
||||
const companyCode = tenantQuery.data?.slug;
|
||||
|
||||
// 해당 슬러그로 사용자 검색
|
||||
const usersQuery = useQuery({
|
||||
queryKey: ["users", { companyCode }],
|
||||
queryFn: () => fetchUsers(100, 0, companyCode),
|
||||
enabled: !!companyCode,
|
||||
});
|
||||
|
||||
const users = usersQuery.data?.items ?? [];
|
||||
|
||||
return (
|
||||
<Card className="mt-6 bg-[var(--color-panel)]">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User size={18} className="text-primary" />
|
||||
Tenant Members ({users.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>NAME</TableHead>
|
||||
<TableHead>EMAIL</TableHead>
|
||||
<TableHead>ROLE</TableHead>
|
||||
<TableHead>STATUS</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground">
|
||||
소속된 사용자가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-semibold">{user.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<Mail size={12} className="text-muted-foreground" />
|
||||
{user.email}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{user.role.replace("_", " ")}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={user.status === "active" ? "default" : "muted"}>
|
||||
{user.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default TenantUsersPage;
|
||||
Reference in New Issue
Block a user