참고
- 파일 브라우저 만들기
- chonky 기본 설정 확인하기
- 액션 추가하고 다크 모드 구현하기
- 커스텀 액션 추가하기
- Chonky 파일 맵 만들기
- 리포지토리의 폴더 정보 저장하기
- 깃허브 리포지토리를 파일 브라우저로 불러오기
- useNavigate로 Toast UI Editor 이동하기
이전 글에서 만든 chonky_map.json을 이용해서 리포지토리의 정보를 파일 브라우저로 불러오자.
chonky_map.json을 얻기 위해 ChonkyLoader 컴포넌트를 만들고, map file을 props로 넘긴다.
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;
}
};
이제 ChonkyBrowser의 demoMap을 props로 넘겨받도록 수정한다.
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를 호출하면
실제 리포지토리의 파일을 이동한 것처럼 구현할 수 있다. (실제 적용에 발생하는 딜레이나 오류는 직접 처리해야 한다.)
'개발 > React' 카테고리의 다른 글
리액트 - recoil로 상태 관리하기 (Managing State with recoil) (0) | 2024.03.22 |
---|---|
리액트 - useNavigate로 Toast UI Editor 이동하기 (Toast UI Editor with Chonky Browser) (1) | 2024.03.16 |
리액트 - 커스텀 액션 추가하기 (Add Custom Actions) (0) | 2024.03.16 |
리액트 - 액션 추가하고 다크 모드 구현하기 (Add ChonkyActions for Dark Mode) (0) | 2024.03.16 |
리액트 - chonky 기본 설정 확인하기 (Default ConfigOptions) (0) | 2024.03.16 |
댓글