본문 바로가기
개발/React

리액트 - 깃허브 리포지토리를 파일 브라우저로 불러오기 (Chonky Browser with GitHub Repository)

by 피로물든딸기 2024. 3. 16.
반응형

리액트 전체 링크

 

참고

- RESTful API로 파일 읽기

- RESTful API로 파일 이름 변경하기

 

- 파일 브라우저 만들기
- chonky 기본 설정 확인하기
- 액션 추가하고 다크 모드 구현하기
커스텀 액션 추가하기
- Chonky 파일 맵 만들기
- 리포지토리의 폴더 정보 저장하기
- 깃허브 리포지토리를 파일 브라우저로 불러오기
useNavigate로 Toast UI Editor 이동하기

 

이전 글에서 만든 chonky_map.json을 이용해서 리포지토리의 정보를 파일 브라우저로 불러오자.

 

chonky_map.json을 얻기 위해 ChonkyLoader 컴포넌트를 만들고, map fileprops로 넘긴다.

chonkyMap이 없는 경우(= undefined)는 렌더링 하지 않는다. 

import React, { useEffect, useState } from "react";
import ChonkyBrowser from "./ChonkyBrowser";

import * as gh from "./githublibrary.js";

const ChonkyLoader = () => {
  const [chonkyMap, setChonkyMap] = useState(undefined);

  const getChonkyMap = async () => {
    let result = await gh.fileRead("actions/config/chonky_map.json");
    if (result === undefined) return;

    let repoMap = JSON.parse(result);
    setChonkyMap(repoMap);
    console.log(repoMap);
  };

  useEffect(() => {
    getChonkyMap();
  }, []);

  return <div>{chonkyMap && <ChonkyBrowser chonkyMap={chonkyMap} />}</div>;
};

export default ChonkyLoader;

 

파일 read API는 다음과 같다. (githublibrary.js)

import { Octokit } from "@octokit/rest";

const myKey = process.env.REACT_APP_MY_TOKEN;
const repo = `YOUR_REPO`;

export const fileRead = async (path) => {
  try {
    const octokit = new Octokit({
      auth: myKey,
    });

    const result = await octokit.request(
      `GET /repos/bloodstrawberry/${repo}/contents/${path}`,
      {
        owner: "bloodstrawberry",
        repo: `${repo}`,
        path: `${path}`,
        encoding: "utf-8",
        decoding: "utf-8",
      }
    );

    return decodeURIComponent(escape(window.atob(result.data.content)));
  } catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};

 

이제 ChonkyBrowserdemoMapprops로 넘겨받도록 수정한다.

props로 넘겨받기 위해 ChonkyBrowser 외부에 있던 demoMap과 메서드내부로 이동하였다.

const ChonkyBrowser = React.memo((props) => {
  ... 
  
  // const demoMap = require("./demo.json");
  const demoMap = props.chonkyMap;
  
  const prepareCustomFileMap = () => {
    const baseFileMap = demoMap.fileMap;
    const rootFolderId = demoMap.rootFolderId;
    return { baseFileMap, rootFolderId };
  };
  
  ...

 

전체 코드는 다음과 같다.

import React, {
  useState,
  useCallback,
  useEffect,
  useRef,
  useMemo,
} from "react";
import {
  setChonkyDefaults,
  ChonkyActions,
  FileHelper,
  FullFileBrowser,
} from "chonky";
import { ChonkyIconFA } from "chonky-icon-fontawesome";
import { customActions } from "./myCustomActions";

import Box from "@mui/material/Box";

const ChonkyBrowser = React.memo((props) => {
  const [darkMode, setDarkMode] = useState(false);
  const toggleDarkMode = () => {
    setDarkMode(!darkMode);
  };

  const fileInputRef = useRef(null);

  // const demoMap = require("./demo.json");
  const demoMap = props.chonkyMap;
  
  const prepareCustomFileMap = () => {
    const baseFileMap = demoMap.fileMap;
    const rootFolderId = demoMap.rootFolderId;
    return { baseFileMap, rootFolderId };
  };

  const useCustomFileMap = () => {
    const { baseFileMap, rootFolderId } = useMemo(prepareCustomFileMap, []);

    const [fileMap, setFileMap] = useState(baseFileMap);
    const [currentFolderId, setCurrentFolderId] = useState(rootFolderId);

    const resetFileMap = useCallback(() => {
      setFileMap(baseFileMap);
      setCurrentFolderId(rootFolderId);
    }, [baseFileMap, rootFolderId]);

    const currentFolderIdRef = useRef(currentFolderId);
    useEffect(() => {
      currentFolderIdRef.current = currentFolderId;
    }, [currentFolderId]);

    const deleteFiles = useCallback((files) => {
      setFileMap((currentFileMap) => {
        const newFileMap = { ...currentFileMap };
        files.forEach((file) => {
          delete newFileMap[file.id];

          if (file.parentId) {
            const parent = newFileMap[file.parentId];
            const newChildrenIds = parent.childrenIds.filter(
              (id) => id !== file.id
            );
            newFileMap[file.parentId] = {
              ...parent,
              childrenIds: newChildrenIds,
              childrenCount: newChildrenIds.length,
            };
          }
        });

        return newFileMap;
      });
    }, []);

    const moveFiles = useCallback((files, source, destination) => {
      setFileMap((currentFileMap) => {
        const newFileMap = { ...currentFileMap };
        const moveFileIds = new Set(files.map((f) => f.id));

        const newSourceChildrenIds = source.childrenIds.filter(
          (id) => !moveFileIds.has(id)
        );
        newFileMap[source.id] = {
          ...source,
          childrenIds: newSourceChildrenIds,
          childrenCount: newSourceChildrenIds.length,
        };

        const newDestinationChildrenIds = [
          ...destination.childrenIds,
          ...files.map((f) => f.id),
        ];
        newFileMap[destination.id] = {
          ...destination,
          childrenIds: newDestinationChildrenIds,
          childrenCount: newDestinationChildrenIds.length,
        };

        files.forEach((file) => {
          newFileMap[file.id] = {
            ...file,
            parentId: destination.id,
          };
        });

        return newFileMap;
      });
    }, []);

    const idCounter = useRef(0);
    const createFolder = useCallback((folderName) => {
      setFileMap((currentFileMap) => {
        const newFileMap = { ...currentFileMap };

        const newFolderId = `new-folder-${idCounter.current++}`;
        newFileMap[newFolderId] = {
          id: newFolderId,
          name: folderName,
          isDir: true,
          modDate: new Date(),
          parentId: currentFolderIdRef.current,
          childrenIds: [],
          childrenCount: 0,
        };

        const parent = newFileMap[currentFolderIdRef.current];
        newFileMap[currentFolderIdRef.current] = {
          ...parent,
          childrenIds: [...parent.childrenIds, newFolderId],
        };

        return newFileMap;
      });
    }, []);

    const createFile = useCallback((fileName) => {
      setFileMap((currentFileMap) => {
        const newFileMap = { ...currentFileMap };

        const newFolderId = `new-folder-${idCounter.current++}`;
        newFileMap[newFolderId] = {
          id: newFolderId,
          name: fileName,
          modDate: new Date(),
          parentId: currentFolderIdRef.current,
          childrenIds: [],
          childrenCount: 0,
        };

        const parent = newFileMap[currentFolderIdRef.current];
        newFileMap[currentFolderIdRef.current] = {
          ...parent,
          childrenIds: [...parent.childrenIds, newFolderId],
        };

        return newFileMap;
      });
    }, []);

    return {
      fileMap,
      currentFolderId,
      setCurrentFolderId,
      resetFileMap,
      deleteFiles,
      moveFiles,
      createFolder,
      createFile,
    };
  };

  const useFiles = (fileMap, currentFolderId) => {
    return useMemo(() => {
      const currentFolder = fileMap[currentFolderId];
      const childrenIds = currentFolder.childrenIds;
      const files = childrenIds.map((fileId) => fileMap[fileId]);
      return files;
    }, [currentFolderId, fileMap]);
  };

  const useFolderChain = (fileMap, currentFolderId) => {
    return useMemo(() => {
      const currentFolder = fileMap[currentFolderId];
      const folderChain = [currentFolder];
      let parentId = currentFolder.parentId;
      while (parentId) {
        const parentFile = fileMap[parentId];
        if (parentFile) {
          folderChain.unshift(parentFile);
          parentId = parentFile.parentId;
        } else {
          break;
        }
      }
      return folderChain;
    }, [currentFolderId, fileMap]);
  };

  const useFileActionHandler = (
    folderChain,
    setCurrentFolderId,
    deleteFiles,
    moveFiles,
    createFolder,
    toggleDarkMode,
    fileInputRef
  ) => {
    return useCallback(
      (data) => {
        if (data.id === ChonkyActions.OpenFiles.id) {
          const { targetFile, files } = data.payload;
          const fileToOpen = targetFile || files[0];
          if (fileToOpen && FileHelper.isDirectory(fileToOpen)) {
            setCurrentFolderId(fileToOpen.id);
            return;
          }
        } else if (data.id === ChonkyActions.DeleteFiles.id) {
          deleteFiles(data.state.selectedFilesForAction);
        } else if (data.id === ChonkyActions.MoveFiles.id) {
          moveFiles(
            data.payload.files,
            data.payload.source,
            data.payload.destination
          );
        } else if (data.id === ChonkyActions.CreateFolder.id) {
          const folderName = prompt("Provide the name for your new folder:");
          if (folderName) createFolder(folderName);
        } else if (data.id === ChonkyActions.ToggleDarkMode.id) {
          toggleDarkMode();
        } else if (data.id === "view") {
          let fileNames = data.state.selectedFiles.map((item) => item.name);
          let dirPath = folderChain.map((item) => item.name).join("/");

          for (let name of fileNames) {
            console.log(`${dirPath}/${name}`);
          }
        } else if (data.id === "upload") {
          fileInputRef.current.click();
        }

        console.log(data);
      },
      [createFolder, deleteFiles, moveFiles, setCurrentFolderId, toggleDarkMode]
    );
  };

  const {
    fileMap,
    currentFolderId,
    setCurrentFolderId,
    deleteFiles,
    moveFiles,
    createFolder,
    createFile,
  } = useCustomFileMap();

  setChonkyDefaults({ iconComponent: ChonkyIconFA });

  const files = useFiles(fileMap, currentFolderId);
  const folderChain = useFolderChain(fileMap, currentFolderId);
  const handleFileAction = useFileActionHandler(
    folderChain,
    setCurrentFolderId,
    deleteFiles,
    moveFiles,
    createFolder,
    toggleDarkMode,
    fileInputRef
  );

  const fileActions = useMemo(
    () => [
      ...customActions,
      ChonkyActions.CreateFolder,
      ChonkyActions.DeleteFiles,
      // ChonkyActions.CopyFiles,
      // ChonkyActions.UploadFiles,
      // ChonkyActions.DownloadFiles,
      ChonkyActions.ToggleDarkMode,
    ],
    []
  );

  const thumbnailGenerator = useCallback(
    (file) =>
      file.thumbnailUrl ? `https://chonky.io${file.thumbnailUrl}` : null,
    []
  );

  const fileUpload = async (e) => {
    let files = e.target.files;
    for (let file of files) {
      if (file === undefined) continue;

      console.log(file.name); // 파일 이름

      createFile(file.name);
      // 실제 파일 내용 read
      // let fileReader = new FileReader();
      // fileReader.readAsText(file, "utf-8"); // or euc-kr
      // fileReader.onload = function () {
      //   console.log(fileReader.result);
      // };
    }
  };

  return (
    <Box sx={{ m: 2 }}>
      <input
        type="file"
        ref={fileInputRef}
        accept=".json,.md"
        style={{ display: "none" }}
        multiple
        onChange={(e) => fileUpload(e)}
      />

      <div style={{ height: 400 }}>
        <FullFileBrowser
          files={files}
          folderChain={folderChain}
          fileActions={fileActions}
          onFileAction={handleFileAction}
          thumbnailGenerator={thumbnailGenerator}
          // disableDefaultFileActions={true} // default false
          // doubleClickDelay={500} // ms
          // disableSelection={true} // default false 파일 선택이 해제됨
          // disableDragAndDrop={true} // 드래그 앤 드랍 기능 off
          // disableDragAndDropProvider={true} // default false, 다른 드래그 앤 드롭은 유지
          // defaultSortActionId={ChonkyActions.SortFilesByDate.id} // or SortFilesByName, SortFilesBySize
          // defaultFileViewActionId={ChonkyActions.EnableListView.id} // or EnableGridView
          // clearSelectionOnOutsideClick={false} // default true 브라우저 외부 클릭 시 파일 선택 해제
          darkMode={darkMode}
          {...props}
        />
      </div>
    </Box>
  );
});

export default ChonkyBrowser;

파일 이동하기

 

Chonky Browser에는 이미 MoveFiles 액션이 정의되어 있다.

  } else if (data.id === ChonkyActions.MoveFiles.id) {
    moveFiles(
      data.payload.files,
      data.payload.source,
      data.payload.destination
    );

 

MoveFiles 액션 발생 시, 깃허브의 파일을 이동하도록 코드를 추가하면 실제로 리포지토리의 파일을 이동할 수 있다.

 

하지만 깃허브의 파일을 이동하는 API는 따로 없다.

RESTful API로 파일 이름 변경하기를 참고하여 파일의 이름을 변경하는 방식으로 API를 호출하면 

실제 리포지토리의 파일을 이동한 것처럼 구현할 수 있다. (실제 적용에 발생하는 딜레이오류는 직접 처리해야 한다.)

반응형

댓글