본문 바로가기
개발/React

React Material - 파일 브라우저에서 폴더 이벤트 추가하기 (Folder Click Event in File Browser)

by 피로물든딸기 2023. 6. 12.
반응형

리액트 전체 링크

 

참고

Mui Tree View로 파일, 폴더 뷰 만들기

Mui Tree View로 파일, 폴더 뷰 만들기 (with Node JS)

- Mui 파일, 폴더 뷰 확장 / 선택하기

Mui로 파일 브라우저 만들기
파일 브라우저에서 파일 다운로드하기

- 파일 브라우저 정렬하기

- 파일 브라우저에서 폴더 이벤트 추가하기

 

이전 글에서 파일을 클릭하면 파일이 다운로드 되었다.

이번에는 폴더를 클릭했을 때, 파일 브라우저를 해당 폴더 내의 파일과 폴더로 변경해보자.

 

즉, 파일 / 폴더 뷰와 브라우저를 연동해보자.


nodeId와 경로 mapping 

 

예를 들어 D:/github/globfiles/def1 경로를 선택했다고 하자.

그러면 expaned에는 최소 4개 = [D:, D:/github, D:/github/globfiles, D:/github/globfiles/def1]에 대한

nodeId가 expanded에 포함되어야 한다.

 

따라서 모든 경로에 대해 nodeId를 mapping 해두는 table을 만들자.

const FileBrowser = () => {
  ...
  
  const [idMap, setIdMap] = useState({});

 

makeDirectories는 아래와 같이 수정한다.

appendChild에는 현재 전체 경로(d)와 tmpIdMap을 넘긴다.

appendChild에서 tmpIdMap과 nodeId를 mapping한다.

그리고 setIdMap으로 설정한다.

  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);
  };

 

appendChild에는 tmpIdMap[path] = nodeId.toString(); 가 추가되었다.

  let nodeId = 0;
  const appendChild = (arr, info, path, tmpIdMap) => {
    if (arr.child === undefined) arr.child = [];
    if (arr.child.findIndex((item) => item.label === info) === -1) {
      console.log(path);
      arr.child.push({ label: info, nodeId });
      tmpIdMap[path] = nodeId.toString();
      nodeId++;
    }
  };

 

makeDirectories에서 경로 D:, D:/github을 추가하였는데, getFiles에서 아래와 같이 root를 선택했기 때문이다.

  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));
  };

expanded, selected 추가

 

Mui 파일, 폴더 뷰 확장 / 선택하기를 참고하여 아래의 코드를 추가한다.

 

먼저 useState로 expended와 selected를 추가하자.

const FileBrowser = () => {
  ...
  
  const [idMap, setIdMap] = useState({});
  const [expanded, setExpanded] = useState([]);
  const [selected, setSelected] = useState([]);

 

그리고 아래의 메서드를 추가한다.

  const handleToggle = (event, ids) => {
    setExpanded(ids);
  }

  const handleSelect = (event, ids) => {
    setSelected(ids);
  }

 

TreeView에 expanded, onNodeToggle, selected, onNodeSelected를 추가한다.

        <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>

 

fileUi에 아래와 같이 만들어둔 mapping table과 useState를 추가한다.

sortFileUI는 폴더 / 파일을 정렬하기 위한 메서드다. (링크 참고)

  {fileUi.map((f, idx) => (
    <FileUI
      key={idx}
      pathInfo={f}
      idMap={idMap}
      tvfunc={{ expanded, setExpanded, setSelected, sortFileUI }}
    />
  ))}

FileUI 변경

 

FileUI에는 onDoubleClick 이벤트를 추가한다.

파일을 클릭하면 파일이 download되고, 폴더를 더블 클릭하면 해당 폴더로 이동하는 동작을 만든다.

그리고 FileBrowser에서 넘겨준 props를 받도록 수정하였다.

const FileUI = ({ pathInfo, idMap, tvfunc }) => {
  return (
    <div
	  ...
      onClick={() => download(pathInfo)}
      onDoubleClick={() => getFileBrowser(pathInfo, idMap, tvfunc)}
    >
      {getMuiIcon(getFileName(pathInfo))}
      ...
    </div>
  );
};

 

getFileBrowser는 다음과 같다.

fileName에 확장자가 없는 경우에만 동작한다.

makeExpandedView는 현재 선택한 경로에 대한 nodeId를 찾아서 expanded를 set한다.

그리고 선택한 폴더와 같은 nodeId를 selected한다.

마지막으로 sortFileUI를 이용하여 폴더와 파일을 가져온다.

const getFileBrowser = (pathInfo, idMap, tvfunc) => {
  if (pathInfo === undefined || pathInfo === "") return;

  let fileName = getFileName(pathInfo);

  if (fileName.includes(".")) return;

  makeExpandedView(pathInfo, idMap, tvfunc);
  tvfunc.setSelected(idMap[pathInfo]);

  let server = `http://192.168.55.120:3002`;
  let path = pathInfo + "/*";

  fetch(`${server}/useGlob?path=${path}`)
    .then((res) => res.json())
    .then((data) => tvfunc.sortFileUI(data.findPath));
}

 

makeExpandedView는 다음과 같다.

현재 expanded에서 선택한 폴더에 대한 expanded를 추가하였다.

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]);
}

 

이제 tree view와 file browser가 잘 연동되는 것을 알 수 있다.

 

전체 코드는 다음과 같다.

 

FileBrowser.js

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";
import FileUI from "./FileUI";

// 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 FileBrowser = () => {
  const [treeInfo, setTreeInfo] = useState({});
  const [fileUi, setFileUI] = useState([]);

  const [idMap, setIdMap] = useState({});
  const [expanded, setExpanded] = useState([]);
  const [selected, setSelected] = 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) {
      console.log(path);
      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);
  };

  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);
  }

  return (
    <div
      style={{
        display: "grid",
        gridTemplateColumns: "20% 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>
        <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;

 

FileUI.js

import React from "react";

import axios from "axios";

import TextSnippetIcon from "@mui/icons-material/TextSnippet";
import DataObjectICON from "@mui/icons-material/DataObject";
import FolderIcon from "@mui/icons-material/Folder";

const getFileName = (path) => {
  if (path === undefined) return undefined;
  let spt = path.split("/");
  return spt[spt.length - 1];
};

const getMuiIcon = (fileName) => {
  if (fileName === undefined) return <TextSnippetIcon sx={{ fontSize: 60 }} />;

  let spt = fileName.split(".");
  if (spt.length === 1)
    return <FolderIcon sx={{ fontSize: 60, left: "50%" }} />;
  if (spt[1] === "json")
    return <DataObjectICON sx={{ fontSize: 60, left: "50%" }} />;
  return <TextSnippetIcon sx={{ fontSize: 60, left: "50%" }} />;
};

const downloadPC = (response, fileName) => {
  const url = window.URL.createObjectURL(
    new Blob([response.data], { type: `${response.headers["content-type"]}` })
  );

  let link = document.createElement("a");
  link.href = url;
  link.setAttribute("download", fileName);

  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);

  window.URL.revokeObjectURL(url); // memory 해제
};

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]);
} 

const download = (pathInfo) => {
  if (pathInfo === undefined || pathInfo === "") return;

  let fileName = getFileName(pathInfo);

  if (fileName.includes(".") === false) return; // 폴더인 경우

  let server = `http://192.168.55.120:3002`;
  axios
    .get(`${server}/downloadFile?filePath=${pathInfo}`, {
      responseType: "arraybuffer",
    })
    .then((res) => {
      downloadPC(res, fileName);
    })
    .catch((error) => console.log(error));
};

const getFileBrowser = (pathInfo, idMap, tvfunc) => {
  if (pathInfo === undefined || pathInfo === "") return;

  let fileName = getFileName(pathInfo);

  if (fileName.includes(".")) return;

  makeExpandedView(pathInfo, idMap, tvfunc);
  tvfunc.setSelected(idMap[pathInfo]);

  let server = `http://192.168.55.120:3002`;
  let path = pathInfo + "/*";

  fetch(`${server}/useGlob?path=${path}`)
    .then((res) => res.json())
    .then((data) => tvfunc.sortFileUI(data.findPath));
}

const FileUI = ({ pathInfo, idMap, tvfunc }) => {
  return (
    <div
      style={{
        display: "inline-block",
        width: "110px",
        height: "120px",
        backgroundColor: "silver",
        textAlign: "center",
        margin: "10px",
        cursor: "pointer",
        verticalAlign: "top",
      }}
      onClick={() => download(pathInfo)}
      onDoubleClick={() => getFileBrowser(pathInfo, idMap, tvfunc)}
    >
      {getMuiIcon(getFileName(pathInfo))}
      <div style={{ width: "110px", height: "40px", wordBreak: "break-all" }}>
        <span
          style={{
            fontSize: "12px",
            backgroundColor: "green",
            cursor: "pointer",
            display: "block",
            height: "50px",
          }}
        >
          {getFileName(pathInfo)}
        </span>
      </div>
    </div>
  );
};

export default FileUI;
반응형

댓글