From dacc7e357efedfb5bcd5a447199f311332f4f59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=98=84=EC=A1=B0?= Date: Fri, 4 Oct 2024 02:55:06 +0900 Subject: [PATCH] =?UTF-8?q?Refactor:=EB=A6=AC=EC=95=A1=ED=8A=B8=20=20?= =?UTF-8?q?=EC=9C=88=EB=8F=84=EC=9A=B0=20=EC=B6=94=EA=B0=80=20=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EA=B7=B8=20=EC=95=A4=20=EB=93=9C=EB=9E=8D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WorkspaceSidebar/ProjectStructure.tsx | 349 ++++++++++-------- frontend/src/hooks/useTreeData.ts | 20 +- 2 files changed, 217 insertions(+), 152 deletions(-) diff --git a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx index 056c451..b6185e4 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { TreeNode } from 'react-treebeard'; import useProjectStore from '@/stores/useProjectStore'; import useCanvasStore from '@/stores/useCanvasStore'; @@ -9,7 +9,22 @@ import AutoLabelButton from './AutoLabelButton'; import useMoveImageQuery from '@/queries/images/useMoveImageQuery'; import { Folder, Image as ImageIcon, Minus, Loader, ArrowDownToLine, Send, CircleSlash, Check } from 'lucide-react'; import { Spinner } from '../ui/spinner'; -import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; + +import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; + +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; + +interface FlatNode extends TreeNode { + depth: number; + isLeaf: boolean; + parent?: FlatNode; + index?: number; +} + +const ItemTypes = { + NODE: 'node', +}; export default function ProjectStructure({ project }: { project: Project }) { const { setProject } = useProjectStore(); @@ -18,11 +33,20 @@ export default function ProjectStructure({ project }: { project: Project }) { const [cursor, setCursor] = useState(null); const moveImageMutation = useMoveImageQuery(); + const containerRef = useRef(null); + const [containerHeight, setContainerHeight] = useState(400); + useEffect(() => { setProject(project); initializeTree(); }, [project, setProject, initializeTree]); + useEffect(() => { + if (containerRef.current) { + setContainerHeight(containerRef.current.clientHeight); + } + }, [containerRef, treeData, isLoading]); + const onToggle = useCallback( async (node: TreeNode, toggled: boolean) => { if (cursor) { @@ -37,15 +61,27 @@ export default function ProjectStructure({ project }: { project: Project }) { if (toggled && (!node.children || node.children.length === 0)) { await fetchNodeData(node); } - node.toggled = toggled; - } - setTreeData((prevData) => ({ ...prevData! })); + const updateNode = (currentNode: TreeNode): TreeNode => { + if (currentNode.id === node.id) { + return { ...currentNode, toggled }; + } + if (currentNode.children) { + return { + ...currentNode, + children: currentNode.children.map(updateNode), + }; + } + return currentNode; + }; + + setTreeData((prevData) => prevData && updateNode(prevData)); + } }, [cursor, fetchNodeData, setImage, setTreeData] ); - const renderStatusIcon = (status: ImageStatus) => { + const renderStatusIcon = useCallback((status: ImageStatus) => { switch (status) { case 'PENDING': return ( @@ -91,124 +127,72 @@ export default function ProjectStructure({ project }: { project: Project }) { /> ); } - }; + }, []); - const renderTree = useCallback( - (nodes: TreeNode[], _parentId: string, level: number = 0) => { - return nodes.map((node, index) => ( - - {(provided) => ( -
-
onToggle(node, !node.toggled)} - > -
- {!node.imageData ? ( - - ) : ( - - )} -
- {node.name} - {node.imageData &&
{renderStatusIcon(node.imageData.status)}
} -
- {node.toggled && node.children && node.children.length > 0 && ( - - {(provided) => ( -
- {renderTree(node.children!, node.id!, level + 1)} - {provided.placeholder} -
- )} -
- )} -
- )} -
- )); - }, - [onToggle] - ); + const flattenTree = useCallback((nodes: TreeNode[], depth: number = 0, parent?: FlatNode): FlatNode[] => { + let flatList: FlatNode[] = []; - const onDragEnd = useCallback( - (result: DropResult) => { - if (!result.destination || !treeData) { - return; - } - - const sourceDroppableId = result.source.droppableId; - const destinationDroppableId = result.destination.droppableId; - const sourceIndex = result.source.index; - const destinationIndex = result.destination.index; - - const findNodeById = (nodes: TreeNode[], id: string): TreeNode | null => { - for (const node of nodes) { - if (node.id === id) { - return node; - } - if (node.children) { - const found = findNodeById(node.children, id); - if (found) return found; - } - } - return null; + nodes.forEach((node, index) => { + const flatNode: FlatNode = { + ...node, + depth, + isLeaf: !node.children || node.children.length === 0, + parent, + index, }; + flatList.push(flatNode); - const sourceParentNode = sourceDroppableId === 'root' ? treeData : findNodeById([treeData], sourceDroppableId); - const destinationParentNode = - destinationDroppableId === 'root' ? treeData : findNodeById([treeData], destinationDroppableId); - - if (!sourceParentNode || !destinationParentNode) { - return; + if (node.toggled && node.children) { + flatList = flatList.concat(flattenTree(node.children, depth + 1, flatNode)); } + }); - const [movedItem] = sourceParentNode.children!.splice(sourceIndex, 1); + return flatList; + }, []); - destinationParentNode.children!.splice(destinationIndex, 0, movedItem); + const flatData = useMemo(() => { + if (!treeData) return []; + return flattenTree([treeData]); + }, [treeData, flattenTree]); - setTreeData({ ...treeData }); + const getItemKey = (index: number, data: FlatNode[]) => data[index].id!; - if (movedItem && movedItem.imageData) { - const moveFolderId = Number(destinationParentNode.id) || 0; - const folderId = Number(sourceParentNode.id) || 0; + const moveNode = useCallback( + (dragItem: FlatNode, hoverItem: FlatNode) => { + const updatedTreeData = (function moveNodeInTree(node: TreeNode): TreeNode { + if (node.id === dragItem.parent?.id) { + const newChildren = node.children?.filter((child) => child.id !== dragItem.id) || []; + return { ...node, children: newChildren }; + } + + if (node.id === hoverItem.parent?.id) { + const newChildren = [...(node.children || [])]; + const hoverIndex = newChildren.findIndex((child) => child.id === hoverItem.id); + newChildren.splice(hoverIndex, 0, { ...dragItem, parent: hoverItem.parent } as FlatNode); + return { ...node, children: newChildren }; + } + + if (node.children) { + return { + ...node, + children: node.children.map(moveNodeInTree), + }; + } + + return node; + })(treeData!); + + setTreeData(updatedTreeData); + + if (dragItem.imageData) { + const moveFolderId = Number(hoverItem.parent?.id) || 0; + const folderId = Number(dragItem.parent?.id) || 0; const projectId = Number(project.id); moveImageMutation.mutate({ projectId, folderId, - imageId: movedItem.imageData.id, + imageId: dragItem.imageData.id, moveRequest: { moveFolderId, }, @@ -218,49 +202,112 @@ export default function ProjectStructure({ project }: { project: Project }) { [treeData, setTreeData, moveImageMutation, project.id] ); + const Row = ({ index, style, data }: ListChildComponentProps) => { + const node = data[index]; + const ref = useRef(null); + + const [, drop] = useDrop({ + accept: ItemTypes.NODE, + drop(item: FlatNode) { + const dragItem = item; + const hoverItem = node; + + if (dragItem.id === hoverItem.id) { + return; + } + + // 드래그가 끝났을 때 노드 이동 처리 + moveNode(dragItem, hoverItem); + }, + }); + + const [{ isDragging }, drag] = useDrag({ + type: ItemTypes.NODE, + item: () => ({ ...node, index }), + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + + drag(drop(ref)); + + return ( +
onToggle(node, !node.toggled)} + > +
+ {!node.imageData ? ( + + ) : ( + + )} +
+ {node.name} + {node.imageData &&
{renderStatusIcon(node.imageData.status)}
} +
+ ); + }; + return ( -
-
-
-
-

{project.type}

-
- {}} - /> -
- {isLoading ? ( -
- -
- ) : !treeData ? ( -
Loading...
- ) : ( - - +
+
+
+
+

{project.type}

+
+ {}} + /> +
+ {isLoading ? ( +
+ +
+ ) : !treeData ? ( +
+ Loading... +
+ ) : ( + - {(provided) => ( -
- {renderTree(treeData.children!, 'root')} - {provided.placeholder} -
- )} - - - )} + {Row} +
+ )} +
+
+ +
-
- -
-
+ ); } diff --git a/frontend/src/hooks/useTreeData.ts b/frontend/src/hooks/useTreeData.ts index ed167b9..84778fb 100644 --- a/frontend/src/hooks/useTreeData.ts +++ b/frontend/src/hooks/useTreeData.ts @@ -31,7 +31,25 @@ export default function useTreeData(projectId: string, initialFolderId: number) node.children = [...childFolders, ...images]; node.loading = false; node.toggled = true; - setTreeData((prevData) => ({ ...prevData! })); + + setTreeData((prevData) => { + if (!prevData) return null; + + const updateNode = (currentNode: TreeNode): TreeNode => { + if (currentNode.id === node.id) { + return { ...node }; + } + if (currentNode.children) { + return { + ...currentNode, + children: currentNode.children.map(updateNode), + }; + } + return currentNode; + }; + + return updateNode(prevData); + }); } catch (error) { node.loading = false; console.error(`Error fetching data for node ${node.id}:`, error);