diff --git a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx index 00bd5ff2..6addc8c2 100644 --- a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx +++ b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx @@ -580,13 +580,15 @@ const TenantTreeRow: React.FC<{ level: number; isRoot: boolean; onRemove: (id: string, name: string) => void; + onMove: (id: string, newParentId: string) => void; isUpdating: boolean; searchTerm?: string; -}> = ({ node, level, isRoot, onRemove, isUpdating, searchTerm }) => { +}> = ({ node, level, isRoot, onRemove, onMove, isUpdating, searchTerm }) => { const navigate = useNavigate(); const [isExpanded, setIsExpanded] = useState(true); const [isUserAddOpen, setIsUserAddOpen] = useState(false); const [isMemberListOpen, setIsMemberListOpen] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); const hasChildren = node.children && node.children.length > 0; // Auto expand if search matches children @@ -613,10 +615,44 @@ const TenantTreeRow: React.FC<{ const TypeIcon = getTenantIcon(node.type); + // DnD Handlers + const handleDragStart = (e: React.DragEvent) => { + if (isRoot) return; + e.dataTransfer.setData("nodeId", node.id); + e.dataTransfer.setData("nodeName", node.name); + e.dataTransfer.effectAllowed = "move"; + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + if (isUpdating) return; + setIsDragOver(true); + }; + + const handleDragLeave = () => { + setIsDragOver(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + const draggedId = e.dataTransfer.getData("nodeId"); + if (!draggedId || draggedId === node.id) return; + onMove(draggedId, node.id); + }; + + const hoverTitle = `${node.name} (${node.type})\n${t("ui.admin.tenants.members.direct", "소속 멤버")}: ${node.memberCount || 0}\n${t("ui.admin.tenants.members.total", "총 멤버")}: ${node.recursiveMemberCount || 0}`; + return ( <>
@@ -676,6 +712,7 @@ const TenantTreeRow: React.FC<{ type="button" className="flex items-center gap-2 cursor-pointer hover:bg-muted p-1.5 rounded-md transition-all group/members w-full text-left" onClick={() => setIsMemberListOpen(true)} + title={t("msg.admin.org.hover_member_info", "클릭하여 멤버 상세 조회")} >
@@ -772,6 +809,7 @@ const TenantTreeRow: React.FC<{ level={level + 1} isRoot={false} onRemove={onRemove} + onMove={onMove} isUpdating={isUpdating} searchTerm={searchTerm} /> @@ -841,6 +879,10 @@ function TenantUserGroupsTab() { const handleAdd = (id: string) => updateParentMutation.mutate({ id, parentId: tenantId }); + const handleMove = (id: string, newParentId: string) => { + if (id === newParentId) return; + updateParentMutation.mutate({ id, parentId: newParentId }); + }; const handleRemove = (id: string, name: string) => { if ( window.confirm( @@ -1084,6 +1126,7 @@ function TenantUserGroupsTab() { level={0} isRoot={true} onRemove={handleRemove} + onMove={handleMove} isUpdating={updateParentMutation.isPending} searchTerm={treeSearchTerm} />