diff --git a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx index d41bb30..18d3f8d 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx @@ -15,7 +15,7 @@ import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import useFolderQuery from '@/queries/folders/useFolderQuery'; import MemoFileStatusIcon from './FileStatusIcon'; - +import moveNodeInTree from '@/utils/moveNodeInTree'; interface FlatNode extends TreeNode { depth: number; isLeaf: boolean; @@ -114,25 +114,10 @@ export default function ProjectStructure({ project }: { project: Project }) { 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 }; - } - - return node.children ? { ...node, children: node.children.map(moveNodeInTree) } : node; - })(treeData!); - - setTreeData(updatedTreeData); + setTreeData(moveNodeInTree(treeData!, dragItem, hoverItem)); if (!dragItem.imageData) return; + moveImageMutation.mutate({ projectId: project.id, folderId: Number(dragItem.parent?.id), @@ -222,13 +207,12 @@ export default function ProjectStructure({ project }: { project: Project }) { onRefetch={refetch} /> - {isLoading ? ( + {isLoading || !treeData ? (
- -
- ) : !treeData ? ( -
- Loading... +
) : ( getFolder(projectId, folderId), - enabled: folderId === 0, - }); -} - -function useChildFolder(projectId: string, folderId: number, enabled: boolean) { - return useQuery({ - queryKey: ['folder', projectId, folderId], - queryFn: () => getFolder(projectId, folderId), - enabled: enabled, + enabled, }); } @@ -25,87 +18,62 @@ export default function useTreeData(projectId: string) { const [currentFolderId, setCurrentFolderId] = useState(null); const { data: rootFolder, isLoading: isRootLoading } = useFolder(projectId, 0); - - const { data: childFolder, isFetching: isChildLoading } = useChildFolder( + const { data: childFolder, isFetching: isChildLoading } = useFolder( projectId, currentFolderId || 0, currentFolderId !== null ); - useEffect(() => { - console.log('root changed'); - if (rootFolder) { - const childFolders: TreeNode[] = - rootFolder.children?.map((child: ChildFolder) => ({ - id: child.id.toString(), - name: child.title, - children: [], - })) || []; + const updateTreeData = useCallback((folder: FolderResponse, isRoot: boolean = false) => { + if (!folder) return; - const images: TreeNode[] = - rootFolder.images?.map((image: ImageResponse) => ({ - id: image.id.toString(), - name: image.imageTitle, - imageData: image, - children: [], - })) || []; + const childNodes = buildTreeNodes(folder); - const rootNode: TreeNode = { - id: rootFolder.id.toString(), - name: rootFolder.title, - children: [...childFolders, ...images], - toggled: true, + setTreeData((prevData) => { + if (isRoot || !prevData) { + return { + id: folder.id.toString(), + name: folder.title, + children: childNodes, + toggled: true, + }; + } + + const updateNode = (currentNode: TreeNode): TreeNode => { + if (currentNode.id !== folder.id.toString()) { + return currentNode.children + ? { ...currentNode, children: currentNode.children.map(updateNode) } + : currentNode; + } + + return { + ...currentNode, + children: childNodes, + toggled: true, + }; }; - setTreeData(rootNode); - } - }, [rootFolder]); + return updateNode(prevData); + }); + }, []); useEffect(() => { - if (childFolder && currentFolderId !== null) { - const childFolders: TreeNode[] = - childFolder.children?.map((child: ChildFolder) => ({ - id: child.id.toString(), - name: child.title, - children: [], - })) || []; + if (!rootFolder) return; + updateTreeData(rootFolder, true); + }, [rootFolder, updateTreeData]); - const images: TreeNode[] = - childFolder.images?.map((image: ImageResponse) => ({ - id: image.id.toString(), - name: image.imageTitle, - imageData: image, - children: [], - })) || []; + useEffect(() => { + if (!childFolder || currentFolderId === null) return; + updateTreeData(childFolder); + }, [childFolder, currentFolderId, updateTreeData]); - setTreeData((prevData) => { - if (!prevData) return null; - - const updateNode = (currentNode: TreeNode): TreeNode => { - if (currentNode.id === currentFolderId.toString()) { - return { - ...currentNode, - children: [...childFolders, ...images], - toggled: true, - }; - } - if (currentNode.children) { - return { - ...currentNode, - children: currentNode.children.map(updateNode), - }; - } - return currentNode; - }; - - return updateNode(prevData); - }); - } - }, [childFolder, currentFolderId]); - - const fetchNodeData = useCallback((node: TreeNode) => { - setCurrentFolderId(Number(node.id)); - }, []); + const fetchNodeData = useCallback( + (node: TreeNode) => { + if (currentFolderId === Number(node.id)) return; + setCurrentFolderId(Number(node.id)); + }, + [currentFolderId] + ); return { treeData, diff --git a/frontend/src/utils/buildTreeNodes.ts b/frontend/src/utils/buildTreeNodes.ts new file mode 100644 index 0000000..d5f5a7b --- /dev/null +++ b/frontend/src/utils/buildTreeNodes.ts @@ -0,0 +1,19 @@ +import { FolderResponse, ChildFolder, ImageResponse } from '@/types'; +import { TreeNode } from 'react-treebeard'; + +export default function buildTreeNodes(folder: FolderResponse): TreeNode[] { + const childFolders: TreeNode[] = folder.children.map((child: ChildFolder) => ({ + id: child.id.toString(), + name: child.title, + children: [], + })); + + const images: TreeNode[] = folder.images.map((image: ImageResponse) => ({ + id: image.id.toString(), + name: image.imageTitle, + imageData: image, + children: [], + })); + + return [...childFolders, ...images]; +} diff --git a/frontend/src/utils/moveNodeInTree.ts b/frontend/src/utils/moveNodeInTree.ts new file mode 100644 index 0000000..3ea6ec0 --- /dev/null +++ b/frontend/src/utils/moveNodeInTree.ts @@ -0,0 +1,42 @@ +import { TreeNode } from 'react-treebeard'; + +interface FlatNode extends TreeNode { + depth: number; + isLeaf: boolean; + parent?: FlatNode; + index?: number; +} + +export default function moveNodeInTree(treeData: TreeNode, dragItem: FlatNode, hoverItem: FlatNode): TreeNode { + if (!treeData) return treeData; + + // 드래그된 항목과 호버된 항목이 동일한 경우 이동 처리하지 않음 + if (dragItem.id === hoverItem.id) return treeData; + + // 부모 노드가 동일한 경우 중복 이동 처리하지 않음 + if (dragItem.parent?.id === hoverItem.parent?.id) return treeData; + + const updateTreeData = (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); + + if (hoverIndex !== -1) { + newChildren.splice(hoverIndex, 0, { ...dragItem, parent: hoverItem.parent } as FlatNode); + } + return { ...node, children: newChildren }; + } + + // 자식 노드가 존재하는 경우 자식 노드들을 순환하면서 업데이트 + return node.children ? { ...node, children: node.children.map(updateTreeData) } : node; + }; + + return updateTreeData(treeData); +}