참고
- Mui Tree View로 파일, 폴더 뷰 만들기 (with Node JS)
- 파일 브라우저에 검색 기능 추가하기
현재까지 만든 파일 브라우저에 Material UI의 Autocomplete으로 파일 검색 기능을 구현해보자.
참고로 더 많은 파일이 잘 검색되는 지 확인하기 위해 여러 파일을 더 추가하였다.
트리 뷰 확장 기능 추가하기
먼저 트리 뷰의 확장 기능을 추가해보자.
검색이 되면 트리 뷰도 펼쳐지면서 해당 파일이나 폴더가 선택되어야 하기 때문이다.
Mui 파일, 폴더 뷰 확장 / 선택하기를 참고하여 아래와 같이 코드를 추가한다.
nodeId를 알 수 있도록 useState로 상태를 관리한다.
const [id, setId] = useState(0);
let nodeId = 0;
const appendChild = (arr, info, path, tmpIdMap) => {
if (arr.child === undefined) arr.child = [];
if (arr.child.findIndex((item) => item.label === info) === -1) {
arr.child.push({ label: info, nodeId });
tmpIdMap[path] = nodeId.toString();
nodeId++;
}
};
const makeDirectories = (directories) => {
...
setId(nodeId);
};
useState로 nodeId 중 가장 큰 번호를 알기 때문에 setExpanded에 적용할 수 있다.
const handleExpandClick = () => {
let fullExpanded = [];
for (let i = 0; i <= id; i++) fullExpanded.push(i.toString());
setExpanded((oldExpanded) =>
oldExpanded.length === 0 ? fullExpanded : []
);
};
전체 코드는 다음과 같다.
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
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";
import FileUI from "./FileUI";
const FileBrowser = () => {
const [treeInfo, setTreeInfo] = useState({});
const [fileUi, setFileUI] = useState([]);
const [idMap, setIdMap] = useState({});
const [expanded, setExpanded] = useState([]);
const [selected, setSelected] = useState([]);
const [id, setId] = useState(0);
let nodeId = 0;
const appendChild = (arr, info, path, tmpIdMap) => {
if (arr.child === undefined) arr.child = [];
if (arr.child.findIndex((item) => item.label === info) === -1) {
arr.child.push({ label: info, nodeId });
tmpIdMap[path] = nodeId.toString();
nodeId++;
}
};
const makeDirectories = (directories) => {
let tmpTreeInfo = {};
let tmpIdMap = {};
let dir = ["D:", "D:/github", ...directories];
for (let d of dir) {
let split = d.split("/");
let len = split.length;
let current = tmpTreeInfo;
for (let i = 0; i < len; i++) {
appendChild(current, split[i], d, tmpIdMap);
current = current.child.find((item) => item.label === split[i]);
}
}
setIdMap(tmpIdMap);
setTreeInfo(tmpTreeInfo);
setId(nodeId);
};
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 isFile = (path) => {
let spt = path.split("/");
return spt[spt.length - 1].includes(".");
};
const sortFileUI = (path) => {
let dir = path.filter((val) => isFile(val) === false);
let files = path.filter((val) => isFile(val));
dir.sort();
files.sort();
setFileUI([...dir, ...files]);
};
const getFilesForFileBrowser = (path) => {
let server = `http://192.168.55.120:3002`;
/* /D:/... 앞의 / 삭제 */
path = path.substring(1, path.length);
fetch(`${server}/useGlob?path=${path}/*`)
.then((res) => res.json())
.then((data) => sortFileUI(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={() => getFilesForFileBrowser(`${parent}/${item.label}`)}
>
{makeTreeItem(item, `${parent}/${item.label}`)}
</TreeItem>
));
};
useEffect(() => {
getFiles();
}, []);
const handleToggle = (event, ids) => {
setExpanded(ids);
};
const handleSelect = (event, ids) => {
setSelected(ids);
};
const handleExpandClick = () => {
let fullExpanded = [];
for (let i = 0; i <= id; i++) fullExpanded.push(i.toString());
setExpanded((oldExpanded) =>
oldExpanded.length === 0 ? fullExpanded : []
);
};
return (
<div
style={{
display: "grid",
gridTemplateColumns: "25% 1% auto",
gridGap: "25px",
width: "100%",
}}
>
{/* <button onClick={getFiles}>test</button> */}
<div>
<a
href="https://bloodstrawberry.tistory.com/1175"
target="_blank"
rel="noreferrer"
>
Node Server 구현 필요
</a>
<Box sx={{ mb: 1 }}>
<Button onClick={handleExpandClick}>
{expanded.length === 0 ? "Expand all" : "Collapse all"}
</Button>
</Box>
<TreeView
expanded={expanded}
onNodeToggle={handleToggle}
selected={selected}
onNodeSelect={handleSelect}
aria-label="file system navigator"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ height: 500, overflowX: "hidden" }}
//sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
>
{makeTreeItem(treeInfo, "")}
</TreeView>
</div>
<div style={{ borderRight: "2px solid black" }} />
<div>
{fileUi.map((f, idx) => (
<FileUI
key={idx}
pathInfo={f}
idMap={idMap}
tvfunc={{ expanded, setExpanded, setSelected, sortFileUI }}
/>
))}
</div>
</div>
);
};
export default FileBrowser;
정상적으로 확장이 잘 되는 것을 알 수 있다.
검색 기능 추가하기
Autocomplete를 참고하여 아래의 코드를 추가한다.
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
Autocomplete value 값과 현재 받아온 파일/폴더 목록을 관리할 dirList를 useState로 선언한다.
const [value, setValue] = useState(null);
const [dirList, setDirList] = useState([]);
makeDirectories에서 directories를 그대로 저장하면 된다.
const makeDirectories = (directories) => {
...
setDirList(directories);
setIdMap(tmpIdMap);
setTreeInfo(tmpTreeInfo);
setId(nodeId);
};
그리고 검색 목록으로 사용할 options로 dirList를 지정한다.
const defaultProps = {
options: dirList,
getOptionLabel: (option) => option,
};
Autocomplete 설정은 다음과 같다.
<div>
<Stack spacing={1} sx={{ width: "80%", marginLeft: 5, paddingBottom: 3 }}>
<Autocomplete
{...defaultProps}
id="auto-complete"
autoComplete
includeInputInList
value={value}
onChange={(event, newValue) => {
setValue(newValue);
}}
renderInput={(params) => (
<TextField {...params} label="Search Files" variant="standard" />
)}
/>
</Stack>
이제 node로 가져온 목록이 보이게 된다.
전체 코드는 다음과 같다.
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
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";
import FileUI from "./FileUI";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
const FileBrowser = () => {
const [treeInfo, setTreeInfo] = useState({});
const [fileUi, setFileUI] = useState([]);
const [idMap, setIdMap] = useState({});
const [expanded, setExpanded] = useState([]);
const [selected, setSelected] = useState([]);
const [id, setId] = useState(0);
const [value, setValue] = useState(null);
const [dirList, setDirList] = useState([]);
let nodeId = 0;
const appendChild = (arr, info, path, tmpIdMap) => {
if (arr.child === undefined) arr.child = [];
if (arr.child.findIndex((item) => item.label === info) === -1) {
arr.child.push({ label: info, nodeId });
tmpIdMap[path] = nodeId.toString();
nodeId++;
}
};
const makeDirectories = (directories) => {
let tmpTreeInfo = {};
let tmpIdMap = {};
let dir = ["D:", "D:/github", ...directories];
for (let d of dir) {
let split = d.split("/");
let len = split.length;
let current = tmpTreeInfo;
for (let i = 0; i < len; i++) {
appendChild(current, split[i], d, tmpIdMap);
current = current.child.find((item) => item.label === split[i]);
}
}
setDirList(directories);
setIdMap(tmpIdMap);
setTreeInfo(tmpTreeInfo);
setId(nodeId);
};
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 isFile = (path) => {
let spt = path.split("/");
return spt[spt.length - 1].includes(".");
};
const sortFileUI = (path) => {
let dir = path.filter((val) => isFile(val) === false);
let files = path.filter((val) => isFile(val));
dir.sort();
files.sort();
setFileUI([...dir, ...files]);
};
const getFilesForFileBrowser = (path) => {
let server = `http://192.168.55.120:3002`;
/* /D:/... 앞의 / 삭제 */
path = path.substring(1, path.length);
fetch(`${server}/useGlob?path=${path}/*`)
.then((res) => res.json())
.then((data) => sortFileUI(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={() => getFilesForFileBrowser(`${parent}/${item.label}`)}
>
{makeTreeItem(item, `${parent}/${item.label}`)}
</TreeItem>
));
};
useEffect(() => {
getFiles();
}, []);
const handleToggle = (event, ids) => {
setExpanded(ids);
};
const handleSelect = (event, ids) => {
setSelected(ids);
};
const handleExpandClick = () => {
let fullExpanded = [];
for (let i = 0; i <= id; i++) fullExpanded.push(i.toString());
setExpanded((oldExpanded) =>
oldExpanded.length === 0 ? fullExpanded : []
);
};
const defaultProps = {
options: dirList,
getOptionLabel: (option) => option,
};
return (
<div>
<Stack spacing={1} sx={{ width: "80%", marginLeft: 5, paddingBottom: 3 }}>
<Autocomplete
{...defaultProps}
id="auto-complete"
autoComplete
includeInputInList
value={value}
onChange={(event, newValue) => {
setValue(newValue);
}}
renderInput={(params) => (
<TextField {...params} label="Search Files" variant="standard" />
)}
/>
</Stack>
<div
style={{
display: "grid",
gridTemplateColumns: "25% 1% auto",
gridGap: "25px",
width: "100%",
}}
>
{/* <button onClick={getFiles}>test</button> */}
<div>
{/* <a
href="https://bloodstrawberry.tistory.com/1175"
target="_blank"
rel="noreferrer"
>
Node Server 구현 필요
</a> */}
<Box sx={{ mb: 1 }}>
<Button onClick={handleExpandClick}>
{expanded.length === 0 ? "Expand all" : "Collapse all"}
</Button>
</Box>
<TreeView
expanded={expanded}
onNodeToggle={handleToggle}
selected={selected}
onNodeSelect={handleSelect}
aria-label="file system navigator"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ height: 500, overflowX: "hidden" }}
//sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
>
{makeTreeItem(treeInfo, "")}
</TreeView>
</div>
<div style={{ borderRight: "2px solid black" }} />
<div>
{fileUi.map((f, idx) => (
<FileUI
key={idx}
pathInfo={f}
idMap={idMap}
tvfunc={{ expanded, setExpanded, setSelected, sortFileUI }}
/>
))}
</div>
</div>
</div>
);
};
export default FileBrowser;
파일 브라우저 기능과 연동하기
Autocomplete로 목록을 선택해도 아무 변화가 없다.
목록을 선택하면 tree view가 확장 및 선택되도록 하고, 폴더를 선택하면 파일 브라우저도 변경해보자.
브라우저를 만드는 함수는 이미 있으므로 onChange에 newValue 값을 넣어주기만 하면 된다.
<Autocomplete
...
onChange={(event, newValue) => {
setValue(newValue);
getFilesForFileBrowser(`/${newValue}`);
}}
/>
이제 폴더를 선택하면 해당 폴더가 가지고 있는 목록이 보인다.
트리 뷰의 확장 기능은 FileUI.js에 이미 구현되어 있다.
const makeExpandedView = (pathInfo, idMap, tvfunc) => {
let tmp = [...tvfunc.expanded];
let spt = pathInfo.split("/");
let tmpPath = "D:";
for(let i = 1; i < spt.length; i++) {
tmpPath += `/${spt[i]}`;
console.log(tmpPath);
if(idMap[tmpPath] === undefined) continue;
if(tmp.includes(idMap[tmpPath])) continue;
tmp.push(idMap[tmpPath]);
}
tvfunc.setExpanded([...tmp]);
}
따로 리팩토링은 하지 않고, FileBrowser.js에 아래와 같이 하나 더 만든다.
setSelected를 이용해 선택 또한 추가하였다.
const makeExpandedView = (pathInfo) => {
let tmp = ["0", "1", ...expanded];
let spt = pathInfo.split("/");
let tmpPath = "D:";
for(let i = 1; i < spt.length; i++) {
tmpPath += `/${spt[i]}`;
if(idMap[tmpPath] === undefined) continue;
if(tmp.includes(idMap[tmpPath])) continue;
tmp.push(idMap[tmpPath]);
}
setSelected(idMap[pathInfo]);
setExpanded([...tmp]);
}
그리고 onChange에 makeExpandedView를 추가한다.
onChange={(event, newValue) => {
setValue(newValue);
getFilesForFileBrowser(`/${newValue}`);
makeExpandedView(newValue);
}}
이제 검색한 결과가 트리 뷰와 잘 연동되는 것을 알 수 있다.
전체 코드는 다음과 같다.
FileBrowser.js
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
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";
import FileUI from "./FileUI";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
const FileBrowser = () => {
const [treeInfo, setTreeInfo] = useState({});
const [fileUi, setFileUI] = useState([]);
const [idMap, setIdMap] = useState({});
const [expanded, setExpanded] = useState([]);
const [selected, setSelected] = useState([]);
const [id, setId] = useState(0);
const [value, setValue] = useState(null);
const [dirList, setDirList] = useState([]);
let nodeId = 0;
const appendChild = (arr, info, path, tmpIdMap) => {
if (arr.child === undefined) arr.child = [];
if (arr.child.findIndex((item) => item.label === info) === -1) {
arr.child.push({ label: info, nodeId });
tmpIdMap[path] = nodeId.toString();
nodeId++;
}
};
const makeDirectories = (directories) => {
let tmpTreeInfo = {};
let tmpIdMap = {};
let dir = ["D:", "D:/github", ...directories];
for (let d of dir) {
let split = d.split("/");
let len = split.length;
let current = tmpTreeInfo;
for (let i = 0; i < len; i++) {
appendChild(current, split[i], d, tmpIdMap);
current = current.child.find((item) => item.label === split[i]);
}
}
setDirList(directories);
setIdMap(tmpIdMap);
setTreeInfo(tmpTreeInfo);
setId(nodeId);
};
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 isFile = (path) => {
let spt = path.split("/");
return spt[spt.length - 1].includes(".");
};
const sortFileUI = (path) => {
let dir = path.filter((val) => isFile(val) === false);
let files = path.filter((val) => isFile(val));
dir.sort();
files.sort();
setFileUI([...dir, ...files]);
};
const getFilesForFileBrowser = (path) => {
let server = `http://192.168.55.120:3002`;
/* /D:/... 앞의 / 삭제 */
path = path.substring(1, path.length);
fetch(`${server}/useGlob?path=${path}/*`)
.then((res) => res.json())
.then((data) => sortFileUI(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={() => getFilesForFileBrowser(`${parent}/${item.label}`)}
>
{makeTreeItem(item, `${parent}/${item.label}`)}
</TreeItem>
));
};
useEffect(() => {
getFiles();
}, []);
const handleToggle = (event, ids) => {
setExpanded(ids);
};
const handleSelect = (event, ids) => {
setSelected(ids);
};
const handleExpandClick = () => {
let fullExpanded = [];
for (let i = 0; i <= id; i++) fullExpanded.push(i.toString());
setExpanded((oldExpanded) =>
oldExpanded.length === 0 ? fullExpanded : []
);
};
const makeExpandedView = (pathInfo) => {
let tmp = ["0", "1", ...expanded];
let spt = pathInfo.split("/");
let tmpPath = "D:";
for(let i = 1; i < spt.length; i++) {
tmpPath += `/${spt[i]}`;
if(idMap[tmpPath] === undefined) continue;
if(tmp.includes(idMap[tmpPath])) continue;
tmp.push(idMap[tmpPath]);
}
setSelected(idMap[pathInfo]);
setExpanded([...tmp]);
}
const defaultProps = {
options: dirList,
getOptionLabel: (option) => option,
};
return (
<div>
<Stack spacing={1} sx={{ width: "80%", marginLeft: 5, paddingBottom: 3 }}>
<Autocomplete
{...defaultProps}
id="auto-complete"
autoComplete
includeInputInList
value={value}
onChange={(event, newValue) => {
setValue(newValue);
getFilesForFileBrowser(`/${newValue}`);
makeExpandedView(newValue);
}}
renderInput={(params) => (
<TextField {...params} label="Search Files" variant="standard" />
)}
/>
</Stack>
<div
style={{
display: "grid",
gridTemplateColumns: "25% 1% auto",
gridGap: "25px",
width: "100%",
}}
>
{/* <button onClick={getFiles}>test</button> */}
<div>
{/* <a
href="https://bloodstrawberry.tistory.com/1175"
target="_blank"
rel="noreferrer"
>
Node Server 구현 필요
</a> */}
<Box sx={{ mb: 1 }}>
<Button onClick={handleExpandClick}>
{expanded.length === 0 ? "Expand all" : "Collapse all"}
</Button>
</Box>
<TreeView
expanded={expanded}
onNodeToggle={handleToggle}
selected={selected}
onNodeSelect={handleSelect}
aria-label="file system navigator"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ height: 500, overflowX: "hidden" }}
//sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
>
{makeTreeItem(treeInfo, "")}
</TreeView>
</div>
<div style={{ borderRight: "2px solid black" }} />
<div>
{fileUi.map((f, idx) => (
<FileUI
key={idx}
pathInfo={f}
idMap={idMap}
tvfunc={{ expanded, setExpanded, setSelected, sortFileUI }}
/>
))}
</div>
</div>
</div>
);
};
export default FileBrowser;
댓글