본문 바로가기
개발/React

리액트 - 커스텀 액션 추가하기 (Add Custom Actions)

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

리액트 전체 링크

 

참고

- https://chonky.io/docs/2.x/file-actions/custom-actions

 

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

 

다음과 같이 커스텀 액션을 추가해 보자.


액션 정의하기

 

myCustomAction.js를 만들고 defineFileActionChonkyIconNameimport 하자.

import { defineFileAction, ChonkyIconName } from "chonky";

 

참고로 ChonkyIcon에는 여러 아이콘이 정의되어 있다.

export declare enum ChonkyIconName {
    loading = "loading",
    dropdown = "dropdown",
    placeholder = "placeholder",
    ...
}

 

Action을 정의하는 예시는 다음과 같다.

idbutton 구성요소를 정의한다.

requiersSelectiontrue로 설정하고 fileFilter 조건을 설정하면 특정 파일 / 폴더에 대해서 액션을 활성화한다.

그리고 toolbarcontextMenu에 나타낼지 여부를 선택할 수 있다.

const viewFileAction = defineFileAction({
  id: "view",
  requiresSelection: true,
  fileFilter: (file) => file && !file.isDir,
  button: {
    name: "View",
    toolbar: false,
    contextMenu: true,
    icon: ChonkyIconName.file
  }
});

 

view 이벤트가 발생할 경우 action에서 id가 "view" 인 것을 알 수 있다.

 

contextMenutrue이기 때문에 메뉴에 나타난다.

또한 fileFilter 조건에 의해, 폴더에서는 비활성화되고 있다.

 

다른 액션은 toolbartrue로 설정해 보자.

const uploadFileAction = defineFileAction({
  id: "upload",
  button: {
    name: "Upload",
    toolbar: true,
    contextMenu: false, 
    icon: ChonkyIconName.upload
  }
});

 

툴바에 Upload 액션이 추가되었다.

 

myCustomActions.js는 다음과 같다.

import { defineFileAction, ChonkyIconName } from "chonky";

const uploadFileAction = defineFileAction({
  id: "upload",
  button: {
    name: "Upload",
    toolbar: true,
    contextMenu: false, 
    icon: ChonkyIconName.upload
  }
});

const viewFileAction = defineFileAction({
  id: "view",
  requiresSelection: true,
  fileFilter: (file) => !file.isDir,
  button: {
    name: "View",
    toolbar: false, 
    contextMenu: true,
    icon: ChonkyIconName.file
  }
});

export const customActions = [
  viewFileAction,
  uploadFileAction
];

 

이제 ChonkyBrowser.jscustomAcitonsimport 한다.

import { customActions } from "./myCustomActions";

 

fileActionscustomActions를 추가하면 된다.

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

선택한 파일의 전체 경로 출력하기 (View)

 

선택한 파일전체 경로를 로그로 출력해 보자.

 

커스텀 액션을 추가하였으므로 useFileActionHandler에 구현을 추가하면 된다.

const useFileActionHandler = (...) => {
  return useCallback(
    (data) => {
      ...
      } else if (data.id === ChonkyActions.ToggleDarkMode.id) {
        toggleDarkMode();
      } else if (data.id === "view") {
        console.log(data);
      }
      
    },
    [createFolder, deleteFiles, moveFiles, setCurrentFolderId, toggleDarkMode]
  );
};

 

파일을 선택하고 view 액션을 실행하면 아래와 같이 파일의 이름이 전달된다.

 

그리고 folderChain에는 현재 브라우저에 대한 directory 정보를 알 수 있다.

현재 브라우저는 Chonkyz Demo / Chonky Source Code / packages / chonky-icon-fontawesome 이 된다.

 

따라서 전체 경로(dirPath) + 파일 이름(fileNames)map으로 정리해서 출력하면 된다.

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

 

useFileActionHandler에서 folderChain을 사용하기 위해 파라미터를 추가하였다.

const useFileActionHandler = (
  folderChain,
  setCurrentFolderId,
  ...
) => {

...

  const handleFileAction = useFileActionHandler(
    folderChain,
    setCurrentFolderId,
    ...
  );

 

파일을 선택하고 View 액션을 실행해 보자.


파일 업로드

 

파일을 업로드하기 위해 useRef를 선언한다.

input을 이용하여 파일을 업로드하는데, input 버튼 자체는 감추기 때문이다.

const fileInputRef = useRef(null);

 

input은 다음과 같다.

json과 md 확장자만 허용하고, 여러 파일(multiple)을 업로드할 수 있도록 설정한다.

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

 

useFileActionHandlerfileInputRef를 추가한다.

  const useFileActionHandler = (
  folderChain,
  setCurrentFolderId,
  ...
  fileInputRef
) => {

...

  const handleFileAction = useFileActionHandler(
    folderChain,
    setCurrentFolderId,
    ...
    fileInputRef
  );

 

업로드 액션이 실행되면 감춰진 inputclick만 하면 된다.

  } else if (data.id === "upload") {
    fileInputRef.current.click();
  }

 

즉, 윈도우 파일 업로드 브라우저가 나오게 된다.

 

이제 구체적으로 fileUpload 이벤트를 구현해 보자.

e.target.files에서 파일의 이름과 파일의 내용을 알 수 있다.

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

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

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

 

실제 파일을 업로드하지 않으므로, 파일 내용을 읽는 부분은 주석 처리하고 파일을 업로드해보자.

파일의 이름이 정상적으로 출력되는 것을 알 수 있다.

 

이제 파일 이름을 얻을 수 있으므로, fileMap을 갱신해 보자.

createFile의 원리는 createFolder와 같고 isDir 부분만 제거하면 된다.

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

 

useCustomFileMapcreateFile도 추가한다.

const useCustomFileMap = () => {
  const createFile = useCallback((fileName) => {
    ...
  }, []);


  return {
    ...
    createFolder,
    createFile,
  };
};

...

  const {
    ...
    createFolder,
    createFile,
  } = useCustomFileMap();

 

fileUpload는 다음과 같이 수정하면 된다.

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

 

이제 파일을 업로드하면 브라우저파일이 추가된다.

 

중복 파일이 있는 경우 등의 예외 처리는 생략하였다.

 

전체 코드는 다음과 같다.

 

ChonkyBrowser.js

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 demoMap = require("./demo.json");

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 ChonkyBrowser = React.memo((props) => {
  const [darkMode, setDarkMode] = useState(false);
  const toggleDarkMode = () => {
    setDarkMode(!darkMode);
  };

  const fileInputRef = useRef(null);

  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, Provider : 다른 드래그 앤 드롭은 유지
          // defaultSortActionId={ChonkyActions.SortFilesByDate.id} // SortFilesByName, SortFilesBySize, SortFilesByDate
          // defaultFileViewActionId={ChonkyActions.EnableListView.id} // EnableGridView, EnableListView
          // clearSelectionOnOutsideClick={false} // default true 브라우저 외부 클릭 시 파일 선택 해제
          darkMode={darkMode}
          {...props}
        />
      </div>
    </Box>
  );
});

export default ChonkyBrowser;

 

myCustomAction.js

import { defineFileAction, ChonkyIconName } from "chonky";

const uploadFileAction = defineFileAction({
  id: "upload",
  button: {
    name: "Upload",
    toolbar: true,
    contextMenu: false, 
    icon: ChonkyIconName.upload
  }
});

const viewFileAction = defineFileAction({
  id: "view",
  requiresSelection: true,
  fileFilter: (file) => !file.isDir,
  button: {
    name: "View",
    toolbar: false, 
    contextMenu: true,
    icon: ChonkyIconName.file
  }
});

export const customActions = [
  viewFileAction,
  uploadFileAction
];
반응형

댓글