Javascript/React

[React] Drag&Drop 을 위한 React-complex-tree

eulBlue 2024. 12. 19. 16:41

📱테스트 환경

"react": "18.3.0",
"react-dom": "18.3.0"

드래그 앤 드랍 기능을 만들기 위해서

여러 라이브러리를 살펴봤지만 폴더구조까지 가진 라이브러리가

딱히 보이지도않았고 맘에 드는게 없었다.

그러다가 react-complex-tree 를 딱 봤는데

 

react-complex-tree

Unopinionated Accessible Tree Component with Multi-Select and Drag-And-Drop. Latest version: 2.4.6, last published: a month ago. Start using react-complex-tree in your project by running `npm i react-complex-tree`. There are 21 other projects in the npm re

www.npmjs.com

내가 원하던 그것 .. 그 자체 ..

사용량도 많고 유지보수도 잘되는 것 같아 이걸로 사용하기로 맘먹었는데

사용방법이 조금 까다로워서 좀 오래걸렸지만 방법은 다음과같다.

npm i react-complex-tree

모듈 받아주고

import React, {useState} from "react";
import {
    ControlledTreeEnvironment,
    Tree,
    TreeItem,
    TreeItemIndex,
    DraggingPosition,
} from "react-complex-tree";
import "react-complex-tree/lib/style-modern.css";

interface TreeItemData extends TreeItem {
    index: TreeItemIndex;
    isFolder?: boolean;
    children: TreeItemIndex[];
    data: string;
}

interface TreeData {
    [key: string]: TreeItemData;
}

interface ItemsProps {
    items: TreeData;
    setItems: any;
}

const CategoryEditor: React.FC<ItemsProps> = ({items, setItems}) => {
    const [viewState, setViewState] = useState({
        expandedItems: Object.keys(items).filter(
            (key) => items[key].isFolder
        ) as TreeItemIndex[],
    });

    const handleDrop = (
        draggedItems: TreeItem<TreeItemIndex>[],
        target: DraggingPosition
    ) => {
        const updatedItems = {...items};
        console.log(target)
        draggedItems.forEach((draggedItem) => {
            // Remove the dragged item from its current parent
            const currentParent = Object.values(updatedItems).find((item) =>
                item.children.includes(draggedItem.index)
            );
            if (currentParent) {
                currentParent.children = currentParent.children.filter(
                    (childId) => childId !== draggedItem.index
                );
            }

            if (target.targetType === "item") {
                // Drop on an item (add to its children if it's a folder)
                const targetItem = updatedItems[target.targetItem as string];
                if (targetItem.isFolder) {
                    targetItem.children.push(draggedItem.index);
                } else {
                    // If the target is not a folder, add the item as its sibling
                    const parentItem = Object.values(updatedItems).find((item) =>
                        item.children.includes(target.targetItem as TreeItemIndex)
                    );
                    if (parentItem) {
                        const index = parentItem.children.indexOf(target.targetItem as TreeItemIndex);
                        parentItem.children.splice(index + 1, 0, draggedItem.index);
                    }
                }
            } else if (target.targetType === "between-items" && target.parentItem) {
                // 두 아이템 사이에 추가
                const parentItem = updatedItems[target.parentItem];
                if (parentItem) {
                    const index = target.childIndex || 0;
                    parentItem.children.splice(index, 0, draggedItem.index);
                }
            } else if (target.targetType === "root") {
                // 루트 레벨에 추가
                updatedItems["root"].children.push(draggedItem.index);
            }
        });

        setItems(updatedItems);
    };

    return (
        <ControlledTreeEnvironment
            items={items}
            getItemTitle={(item) => item.data}
            viewState={{
                "tree-2": {
                    expandedItems: viewState.expandedItems,
                },
            }}
            onExpandItem={(item, treeId) => {
                setViewState((prev) => ({
                    ...prev,
                    expandedItems: [...prev.expandedItems, item.index],
                }));
            }}
            onCollapseItem={(item, treeId) => {
                setViewState((prev) => ({
                    ...prev,
                    expandedItems: prev.expandedItems.filter((id) => id !== item.index),
                }));
            }}
            onDrop={handleDrop} // 드래그 앤 드롭 이벤트 핸들러
            canDragAndDrop={true}
            canDropOnFolder={true}
            canReorderItems={true}
        >
            <Tree treeId="tree-2" rootItem="root" treeLabel="Tree Example"/>
        </ControlledTreeEnvironment>
    );
};

export default CategoryEditor;

이렇게 만들어주고 나면 어느 페이지에서건

데이터만 넣어주면 끝!

const Category = () => {
    const [categories, setCategories] = useState({});
    
    return (
    	<CateListBox>
        	<CategoryEditor items={categories} setItems={setCategories}/>
   		</CateListBox>
    )
}

이런식으로 작성해주면 완성된다~

해당 라이브러리를 사용하는거기 때문에 데이터 구조는 반드시 지켜서 사용해줘야한다.