feat: tree 공통 컴포넌트 생성

This commit is contained in:
2025-03-07 18:37:25 +09:00
parent c0abd6d53f
commit 811c6e62db
11 changed files with 332 additions and 7 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -1,6 +1,10 @@
export const images = {
logo: require("./logo.png"),
menuBtn: require("./mob_menuBtn.svg"),
menuBtnClose: require("./mob_menuBtn_close.svg"),
iconFolderOff: require("./icon_folder_off.png"),
iconFolderOn: require("./icon_folder_on.png"),
iconFile: require("./icon_file.png"),
} as const;
export type ImageType = keyof typeof images;
+3
View File
@@ -26,6 +26,7 @@
},
"dependencies": {
"@common/assets": "workspace:*",
"@minoru/react-dnd-treeview": "^3.5.0",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.6",
@@ -47,6 +48,8 @@
"lucide-react": "^0.474.0",
"react-chartjs-2": "^5.3.0",
"react-day-picker": "^8.10.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7"
},
@@ -0,0 +1,39 @@
import React from "react";
import { type NodeModel } from "@minoru/react-dnd-treeview";
import { type CustomData } from "./types";
import { TypeIcon } from "./TypeIcon";
type Props = {
node: NodeModel<CustomData>;
depth: number;
isOpen: boolean;
onToggle: (id: NodeModel["id"]) => void;
};
export const CustomNode: React.FC<Props> = (props) => {
const { droppable, id, text } = props.node;
const indent = props.depth * 24;
const handleToggle = (e: React.MouseEvent) => {
if (!droppable) return;
e.stopPropagation();
props.onToggle(id);
};
return (
<div
style={{ paddingInlineStart: indent }}
className="flex cursor-pointer items-center gap-3 rounded-sm p-2 hover:bg-accent"
onClick={handleToggle}
>
<div>
<div>
<TypeIcon droppable={droppable} isOpen={props.isOpen} />
</div>
</div>
<div>
<span className="text-[1.4rem] font-medium text-label">{text}</span>
</div>
</div>
);
};
@@ -0,0 +1,19 @@
import React from "react";
import { images } from "@common/assets";
type Props = {
droppable?: boolean;
isOpen?: boolean;
};
export const TypeIcon: React.FC<Props> = (props) => {
if (props.droppable) {
return (
<img
src={props.isOpen ? images.iconFolderOn : images.iconFolderOff}
width={30}
/>
);
}
return <img src={images.iconFile} width={22} />;
};
@@ -0,0 +1,38 @@
[
{
"id": 1,
"parent": 0,
"droppable": true,
"text": "Folder 1"
},
{
"id": 2,
"parent": 1,
"droppable": false,
"text": "File 1-1"
},
{
"id": 3,
"parent": 1,
"droppable": false,
"text": "File 1-2"
},
{
"id": 4,
"parent": 0,
"droppable": true,
"text": "Folder 2"
},
{
"id": 5,
"parent": 4,
"droppable": true,
"text": "Folder 2-1"
},
{
"id": 6,
"parent": 5,
"droppable": false,
"text": "File 2-1-1"
}
]
@@ -0,0 +1,68 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { DndProvider } from "react-dnd";
import {
Tree,
MultiBackend,
getBackendOptions,
type NodeModel,
} from "@minoru/react-dnd-treeview";
import { type CustomData } from "./types";
import { CustomNode } from "./CustomNode";
import SampleData from "./__mock__.json";
const meta: Meta<typeof Tree> = {
title: "UI/Tree",
component: Tree,
parameters: {
docs: {
description: {
component: `
- [Docs](https://github.com/minop1205/react-dnd-treeview/blob/next/README.md)
`,
},
},
},
};
export default meta;
type Story = StoryObj<typeof Tree>;
export const Default: Story = {
render: () => <TreeView />,
};
function TreeView() {
const [treeData, setTreeData] = useState<NodeModel<CustomData>[]>(SampleData);
const handleDrop = (newTree: NodeModel<CustomData>[]) => setTreeData(newTree);
return (
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<div>
<Tree<CustomData>
tree={treeData}
rootId={0}
render={(
node: NodeModel<CustomData>,
{ depth, isOpen, onToggle },
) => (
<CustomNode
node={node}
depth={depth}
isOpen={isOpen}
onToggle={onToggle}
/>
)}
onDrop={handleDrop}
classes={{
root: "",
container: "flex flex-col",
listItem: "",
dropTarget: "bg-primary rounded-sm",
draggingSource: "",
placeholder: "",
}}
/>
</div>
</DndProvider>
);
}
@@ -0,0 +1,4 @@
export type CustomData = {
fileType: string;
fileSize: string;
};
+157 -7
View File
@@ -121,6 +121,9 @@ importers:
'@common/assets':
specifier: workspace:*
version: link:../assets
'@minoru/react-dnd-treeview':
specifier: ^3.5.0
version: 3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-accordion':
specifier: ^1.2.3
version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
@@ -184,6 +187,12 @@ importers:
react-day-picker:
specifier: ^8.10.1
version: 8.10.1(date-fns@3.6.0)(react@18.3.1)
react-dnd:
specifier: ^16.0.1
version: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1)
react-dnd-html5-backend:
specifier: ^16.0.1
version: 16.0.1
tailwind-merge:
specifier: ^3.0.1
version: 3.0.2
@@ -228,7 +237,7 @@ importers:
specifier: ^8.5.5
version: 8.6.0(storybook@8.6.0)
'@types/node':
specifier: ^22.13.5
specifier: ^22.13.1
version: 22.13.5
'@vitejs/plugin-react':
specifier: ^4.3.4
@@ -457,7 +466,6 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
dev: true
/@babel/template@7.26.9:
resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==}
@@ -1360,6 +1368,10 @@ packages:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
/@juggle/resize-observer@3.4.0:
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
dev: false
/@kurkle/color@0.3.4:
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
dev: false
@@ -1375,6 +1387,27 @@ packages:
react: 18.3.1
dev: true
/@minoru/react-dnd-treeview@3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-kDHGQ0UK18w6ie5EH68NynATSP9BAzNLmWBXuVSoaGkraWWFdEdAWqQxAZiMorAixFu40MzYkn2N9/Gam6q17w==}
peerDependencies:
react: ^18.3.1
react-dnd: 15.x || 16.x
react-dom: ^18.3.1
dependencies:
'@juggle/resize-observer': 3.4.0
dnd-multi-backend: 8.1.2(dnd-core@16.0.1)
framer-motion: 11.18.2(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dnd: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1)
react-dnd-html5-backend: 16.0.1
react-dnd-touch-backend: 16.0.1
react-dom: 18.3.1(react@18.3.1)
react-use-measure: 2.1.7(react-dom@18.3.1)(react@18.3.1)
transitivePeerDependencies:
- '@emotion/is-prop-valid'
- dnd-core
dev: false
/@next/env@15.1.7:
resolution: {integrity: sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==}
dev: false
@@ -2225,6 +2258,18 @@ packages:
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
dev: false
/@react-dnd/asap@5.0.2:
resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==}
dev: false
/@react-dnd/invariant@4.0.2:
resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==}
dev: false
/@react-dnd/shallowequal@4.0.2:
resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==}
dev: false
/@rollup/pluginutils@5.1.4:
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
engines: {node: '>=14.0.0'}
@@ -2928,7 +2973,6 @@ packages:
resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==}
dependencies:
undici-types: 6.20.0
dev: true
/@types/node@22.13.9:
resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==}
@@ -4098,6 +4142,22 @@ packages:
/dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
/dnd-core@16.0.1:
resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==}
dependencies:
'@react-dnd/asap': 5.0.2
'@react-dnd/invariant': 4.0.2
redux: 4.2.1
dev: false
/dnd-multi-backend@8.1.2(dnd-core@16.0.1):
resolution: {integrity: sha512-KPDVEsiM+6gNEegqZYTWJQgJxYV4vB91tUrvoKJjaS0wwWqT/jNU0P7xJAwCue/cbasJNvk2dFZH7tC+bjX1Rg==}
peerDependencies:
dnd-core: ^16.0.1
dependencies:
dnd-core: 16.0.1
dev: false
/doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@@ -4745,7 +4805,6 @@ packages:
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-glob@3.3.1:
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
@@ -4872,6 +4931,27 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true
/framer-motion@11.18.2(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.3.1
react-dom: ^18.3.1
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
dependencies:
motion-dom: 11.18.1
motion-utils: 11.18.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tslib: 2.8.1
dev: false
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -5083,6 +5163,12 @@ packages:
dependencies:
function-bind: 1.1.2
/hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies:
react-is: 16.13.1
dev: false
/ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -5653,6 +5739,16 @@ packages:
node-source-walk: 7.0.1
dev: true
/motion-dom@11.18.1:
resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==}
dependencies:
motion-utils: 11.18.1
dev: false
/motion-utils@11.18.1:
resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
dev: false
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
@@ -6216,6 +6312,44 @@ packages:
react: 18.3.1
dev: false
/react-dnd-html5-backend@16.0.1:
resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
dependencies:
dnd-core: 16.0.1
dev: false
/react-dnd-touch-backend@16.0.1:
resolution: {integrity: sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==}
dependencies:
'@react-dnd/invariant': 4.0.2
dnd-core: 16.0.1
dev: false
/react-dnd@16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==}
peerDependencies:
'@types/hoist-non-react-statics': '>= 3.3.1'
'@types/node': '>= 12'
'@types/react': ^18.3.18
react: ^18.3.1
peerDependenciesMeta:
'@types/hoist-non-react-statics':
optional: true
'@types/node':
optional: true
'@types/react':
optional: true
dependencies:
'@react-dnd/invariant': 4.0.2
'@react-dnd/shallowequal': 4.0.2
'@types/node': 22.13.5
'@types/react': 18.3.18
dnd-core: 16.0.1
fast-deep-equal: 3.1.3
hoist-non-react-statics: 3.3.2
react: 18.3.1
dev: false
/react-docgen-typescript@2.2.2(typescript@5.6.3):
resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==}
peerDependencies:
@@ -6253,7 +6387,6 @@ packages:
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
/react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
@@ -6315,6 +6448,19 @@ packages:
tslib: 2.8.1
dev: false
/react-use-measure@2.1.7(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
peerDependencies:
react: ^18.3.1
react-dom: ^18.3.1
peerDependenciesMeta:
react-dom:
optional: true
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -6356,6 +6502,12 @@ packages:
strip-indent: 3.0.0
dev: true
/redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
dependencies:
'@babel/runtime': 7.26.9
dev: false
/reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
@@ -6372,7 +6524,6 @@ packages:
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: true
/regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
@@ -7305,7 +7456,6 @@ packages:
/undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
dev: true
/unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}