참고
- glob으로 파일, 폴더 목록 찾기
- Mui Tree View로 파일, 폴더 뷰 만들기
- Mui Tree View로 파일, 폴더 뷰 만들기 (with Node JS)
- Mui로 파일 브라우저 만들기
- 파일 브라우저 만들기 (with Node JS)
- 파일 브라우저에서 파일 다운로드하기
위의 링크에서 코드를 확장하여 실제 외부의 경로에 대해 Tree View를 만들어보자.
외부 경로에 대한 폴더와 파일의 목록은 아래와 같다.
Node JS 설정
glob으로 파일, 폴더 목록 찾기에 이어서 코드를 수정한다.
먼저 cors를 설치한다.
$ npm install cors
그리고 server.js에 cors를 적용한다.
const express = require("express");
const app = express();
const cors = require("cors");
app.use(cors());
const useGlob = require("./routes/useGlob");
app.use("/useGlob", useGlob);
app.listen(3002, () =>
console.log("Node.js Server is running on port 3002...")
);
glob으로 파일, 폴더 목록 찾기를 참고하여 useGlob.js은 아래와 같이 수정한다.
root는 query를 이용하여 react에서 경로를 줄 예정이고, 파일과 폴더를 모두 가져오도록 수정하였다.
const express = require("express");
const router = express.Router();
const glob = require("glob");
router.get("/", (req, res) => {
//let root = `D:\\github\\globfiles\\**`;
let root = req.query.path;
glob(root, function (error, findPath) {
if (error) {
console.log(error);
res.send({ error });
return;
}
console.log(findPath);
res.send({ findPath });
});
});
module.exports = router;
리액트에서 Node JS 반영사항 확인
Mui Tree View로 파일, 폴더 뷰 만들기에 이어서 코드를 추가한다.
TreeViewExample.js에 아래의 코드를 추가한다.
fetch를 이용하여 node server에서 glob으로 경로를 가져온다.
const TreeViewExample = () => {
const getFiles = () => {
let server = `http://192.168.55.120:3002`;
let path = `D:\\github\\globfiles\\**`;
fetch(`${server}/useGlob?path=${path}`)
.then((res) => res.json())
.then((data) => console.log(data));
};
return (
<div>
<button onClick={getFiles}>test</button>
<TreeView
버튼에 getFiles를 추가해서 클릭하면 아래와 같이 콘솔에 설정한 경로의 모든 폴더와 파일이 나오게 된다.
Tree 만들기
주어진 경로 path가 주어졌을 때, glob을 이용하여 해당 경로 하위의 모든 파일과 폴더를 얻을 수 있었다.
이제 해당 경로를 이용해 아래의 object를 만들어야 한다.
위와 같이 해당 폴더 하위 목록을 child로 만든다.
그리고 그 child 내부에도 다시 child가 있는 식이다.
그러면 TreeItem을 recursive한 방법으로 만들 수 있다.
콘솔에 나온 로그를 JSON.stringify(data)로 출력하면 다음과 같다.
{
child: [
{
label: "D:",
nodeId: 0,
child: [
{
label: "github",
nodeId: 1,
child: [
{
label: "globfiles",
nodeId: 2,
child: [
{
label: "abc1",
nodeId: 3,
child: [
{ label: "abc1_jsonfile1.json", nodeId: 4 },
{ label: "abc1_jsonfile2.json", nodeId: 5 },
{ label: "abc1_textfile1.txt", nodeId: 6 },
{ label: "abc1_textfile2.txt", nodeId: 7 },
{
label: "abc2",
nodeId: 8,
child: [
{ label: "abc2_jsonfile.json", nodeId: 12 },
{
label: "abc3",
nodeId: 13,
child: [
{ label: "abc3_jsonfile.json", nodeId: 14 },
{ label: "abc3_textfile.txt", nodeId: 15 },
],
},
],
},
{
label: "abc2_2",
nodeId: 9,
child: [
{ label: "abc2_2_jsonfile.json", nodeId: 10 },
{ label: "abc2_2_textfile.txt", nodeId: 11 },
],
},
],
},
{
label: "def1",
nodeId: 16,
child: [
{ label: "def_jsonfile1.json", nodeId: 17 },
{ label: "def_jsonfile2.json", nodeId: 18 },
{ label: "def_textfile1.txt", nodeId: 19 },
{ label: "def_textfile2.txt", nodeId: 20 },
],
},
{
label: "ghi1",
nodeId: 21,
child: [
{
label: "ghi2",
nodeId: 22,
child: [
{ label: "ghi2_jsonfile1.json", nodeId: 23 },
{ label: "ghi2_jsonfile2.json", nodeId: 24 },
{ label: "ghi2_textfile1.txt", nodeId: 25 },
{ label: "ghi2_textfile2.txt", nodeId: 26 },
],
},
],
},
{ label: "jsonfile1.json", nodeId: 27 },
{ label: "jsonfile2.json", nodeId: 28 },
{ label: "textfile1.txt", nodeId: 29 },
{ label: "textfile2.txt", nodeId: 30 },
],
},
],
},
],
},
],
}
TreeItem 만들기
먼저 getFiles 함수를 수정한다.
useGlob으로 찾아온 data에서 findPath에 폴더와 파일 목록이 존재한다.
const getFiles = () => {
let server = `http://192.168.55.120:3002`;
let path = `D:\\github\\globfiles\\**`;
fetch(`${server}/useGlob?path=${path}`)
.then((res) => res.json())
.then((data) => makeDirectories(data.findPath));
};
이제 주어진 경로의 목록을 돌면서 해당 경로가 존재하지 않을 경우에만 추가한다.
예를 들어 경로가 a/b/c 라면
최초로 child에 [a]가 추가되고, 해당 a의 child에 [b], 그리고 b의 child에 [c]가 되도록 만든다.
그리고 다음 경로가 a/b/d 라면 a의 child인 b의 child에 [c, d]가 되어야 한다.
경로를 모두 / 로 분해한 뒤 current 변수에서 child로 넘어가면서 처리하였다.
해당 경로의 존재 판별은 appendChild에서 처리한다.
그리고 TreeItem에는 unique한 nodeId가 필요하기 때문에 nodeId를 매번 증가시키도록 하였다.
let tmpTreeInfo = {};
let nodeId = 0;
const appendChild = (arr, info) => {
if (arr.child === undefined) arr.child = [];
if (arr.child.findIndex((item) => item.label === info) === -1) {
arr.child.push({ label: info, nodeId });
nodeId++;
}
};
const makeDirectories = (directories) => {
tmpTreeInfo = {};
for (let d of directories) {
let split = d.split("/");
let len = split.length;
let current = tmpTreeInfo;
for (let i = 0; i < len; i++) {
appendChild(current, split[i]);
current = current.child.find((item) => item.label === split[i]);
}
}
console.log(tmpTreeInfo);
setTreeInfo(tmpTreeInfo);
};
원래 예시의 TreeItem의 구성을 보면 재귀적으로 TreeItem이 구성된 것을 알 수 있다.
<TreeItem nodeId="1" label="Applications">
<TreeItem nodeId="2" label="Calendar" />
</TreeItem>
<TreeItem nodeId="5" label="Documents">
<TreeItem nodeId="10" label="OSS" />
<TreeItem nodeId="6" label="MUI" disabled={true}>
<TreeItem nodeId="8" label="index.js" />
</TreeItem>
</TreeItem>
따라서 TreeItem을 재귀호출을 이용하여 구성하면 된다.
child가 없는 경우는 더 이상 Item을 만들지 않고, child가 존재하면 map을 이용해 TreeItem을 만든다.
nodeId는 number지만 mui에서 string을 요구하기 때문에 toString으로 문자열 처리를 하였다.
const makeTreeItem = (info) => {
if (info.child === undefined) return;
return info.child.map((item, idx) => (
<TreeItem key={idx} nodeId={item.nodeId.toString()} label={item.label}>
{makeTreeItem(item)}
</TreeItem>
));
};
이제 test 버튼을 클릭하면 D:\github\globfiles 경로가 TreeView로 보이게 된다.
마무리
먼저 버튼을 눌렀을 때가 아니라, 해당 페이지에 들어오면 경로가 보이도록 하자.
useEffect를 이용하면 간단히 처리할 수 있다.
useEffect(() => {
getFiles();
}, []);
return (
<div>
{/* <button onClick={getFiles}>test</button> */}
그리고 해당 TreeItem을 클릭하였을 때, 전체 경로가 나오도록 하자.
전체 경로를 알고 있어야 실제 파일이나 폴더에 다시 접근할 수 있다.
여기서는 로그만 출력하도록 처리한다.
해당 경로를 재귀호출할 때, 부모 경로를 계속 저장해서 넘기면 쉽게 처리할 수 있다.
const makeTreeItem = (info, parent) => {
if (info.child === undefined) return;
return info.child.map((item, idx) => (
<TreeItem
key={idx}
nodeId={item.nodeId.toString()}
label={item.label}
onClick={() => console.log(`${parent}/${item.label}`)}
>
{makeTreeItem(item, `${parent}/${item.label}`)}
</TreeItem>
));
};
...
<TreeView
aria-label="file system navigator"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
//sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
>
{makeTreeItem(treeInfo, "")}
</TreeView>
전체 코드는 다음과 같다.
import React, { useEffect, useState } from "react";
import TreeView from "@mui/lab/TreeView";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import TreeItem from "@mui/lab/TreeItem";
const TreeViewExample2 = () => {
const [treeInfo, setTreeInfo] = useState({});
let tmpTreeInfo = {};
let nodeId = 0;
const appendChild = (arr, info) => {
if (arr.child === undefined) arr.child = [];
if (arr.child.findIndex((item) => item.label === info) === -1) {
arr.child.push({ label: info, nodeId });
nodeId++;
}
};
const makeDirectories = (directories) => {
tmpTreeInfo = {};
for (let d of directories) {
let split = d.split("/");
let len = split.length;
let current = tmpTreeInfo;
for (let i = 0; i < len; i++) {
appendChild(current, split[i]);
current = current.child.find((item) => item.label === split[i]);
}
}
console.log(tmpTreeInfo);
setTreeInfo(tmpTreeInfo);
};
const getFiles = () => {
let server = `http://192.168.55.120:3002`;
let path = `D:\\github\\globfiles\\**`;
fetch(`${server}/useGlob?path=${path}`)
.then((res) => res.json())
.then((data) => makeDirectories(data.findPath));
};
const makeTreeItem = (info, parent) => {
if (info.child === undefined) return;
return info.child.map((item, idx) => (
<TreeItem
key={idx}
nodeId={item.nodeId.toString()}
label={item.label}
onClick={() => console.log(`${parent}/${item.label}`)}
>
{makeTreeItem(item, `${parent}/${item.label}`)}
</TreeItem>
));
};
useEffect(() => {
getFiles();
}, []);
return (
<div>
{/* <button onClick={getFiles}>test</button> */}
<TreeView
aria-label="file system navigator"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
//sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
>
{makeTreeItem(treeInfo, "")}
</TreeView>
</div>
);
};
export default TreeViewExample2;
배포용 코드
위의 코드는 local에 만든 node server와 통신한다.
배포용 코드는 JSON.stringify()로 만든 data를 local에 추가하고 TreeView만 보이도록 수정하였다.
const getFiles = () => {
setTreeInfo(localData);
return;
};
전체 코드는 다음과 같다.
import React, { useEffect, useState } from "react";
import TreeView from "@mui/lab/TreeView";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import TreeItem from "@mui/lab/TreeItem";
let localData = {
child: [
{
label: "D:",
nodeId: 0,
child: [
{
label: "github",
nodeId: 1,
child: [
{
label: "globfiles",
nodeId: 2,
child: [
{
label: "abc1",
nodeId: 3,
child: [
{ label: "abc1_jsonfile1.json", nodeId: 4 },
{ label: "abc1_jsonfile2.json", nodeId: 5 },
{ label: "abc1_textfile1.txt", nodeId: 6 },
{ label: "abc1_textfile2.txt", nodeId: 7 },
{
label: "abc2",
nodeId: 8,
child: [
{ label: "abc2_jsonfile.json", nodeId: 12 },
{
label: "abc3",
nodeId: 13,
child: [
{ label: "abc3_jsonfile.json", nodeId: 14 },
{ label: "abc3_textfile.txt", nodeId: 15 },
],
},
],
},
{
label: "abc2_2",
nodeId: 9,
child: [
{ label: "abc2_2_jsonfile.json", nodeId: 10 },
{ label: "abc2_2_textfile.txt", nodeId: 11 },
],
},
],
},
{
label: "def1",
nodeId: 16,
child: [
{ label: "def_jsonfile1.json", nodeId: 17 },
{ label: "def_jsonfile2.json", nodeId: 18 },
{ label: "def_textfile1.txt", nodeId: 19 },
{ label: "def_textfile2.txt", nodeId: 20 },
],
},
{
label: "ghi1",
nodeId: 21,
child: [
{
label: "ghi2",
nodeId: 22,
child: [
{ label: "ghi2_jsonfile1.json", nodeId: 23 },
{ label: "ghi2_jsonfile2.json", nodeId: 24 },
{ label: "ghi2_textfile1.txt", nodeId: 25 },
{ label: "ghi2_textfile2.txt", nodeId: 26 },
],
},
],
},
{ label: "jsonfile1.json", nodeId: 27 },
{ label: "jsonfile2.json", nodeId: 28 },
{ label: "textfile1.txt", nodeId: 29 },
{ label: "textfile2.txt", nodeId: 30 },
],
},
],
},
],
},
],
}
const TreeViewExample2 = () => {
const [treeInfo, setTreeInfo] = useState({});
/*
let tmpTreeInfo = {};
let nodeId = 0;
const appendChild = (arr, info) => {
if (arr.child === undefined) arr.child = [];
if (arr.child.findIndex((item) => item.label === info) === -1) {
arr.child.push({ label: info, nodeId });
nodeId++;
}
};
const makeDirectories = (directories) => {
tmpTreeInfo = {};
for (let d of directories) {
let split = d.split("/");
let len = split.length;
let current = tmpTreeInfo;
for (let i = 0; i < len; i++) {
appendChild(current, split[i]);
current = current.child.find((item) => item.label === split[i]);
}
}
console.log(tmpTreeInfo);
setTreeInfo(tmpTreeInfo);
};
*/
const getFiles = () => {
setTreeInfo(localData);
return;
// let server = `http://192.168.55.120:3002`;
// let path = `D:\\github\\globfiles\\**`;
// fetch(`${server}/useGlob?path=${path}`)
// .then((res) => res.json())
// .then((data) => makeDirectories(data.findPath));
};
const makeTreeItem = (info, parent) => {
if (info.child === undefined) return;
return info.child.map((item, idx) => (
<TreeItem
key={idx}
nodeId={item.nodeId.toString()}
label={item.label}
onClick={() => console.log(`${parent}/${item.label}`)}
>
{makeTreeItem(item, `${parent}/${item.label}`)}
</TreeItem>
));
};
useEffect(() => {
getFiles();
}, []);
return (
<div>
{/* <button onClick={getFiles}>test</button> */}
<TreeView
aria-label="file system navigator"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
//sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
>
{makeTreeItem(treeInfo, "")}
</TreeView>
</div>
);
};
export default TreeViewExample2;
결과는 링크에서 확인하자.
댓글