깃허브 데스크탑으로 프로젝트 관리하기 강의 오픈!! (인프런 바로가기)
참고
- RESTful API로 파일 읽기
- RESTful API로 파일 쓰기
- Toast UI 에디터로 깃허브 마크다운 저장하기
- Toast UI 에디터로 이미지를 포함한 깃허브 마크다운 저장하기
- Socket.IO로 Toast UI Editor 동시 편집하기
토스트 UI 에디터로 로컬이 아닌 깃허브에 직접 파일을 저장하고 편집해 보자.
https://github.com/bloodstrawberry/auto-test/blob/main/README.md
먼저 게시글을 보는 모드(Viewer)와 편집 모드(Edit)를 구분하자.
Editor가 아닌 편집된 내용(게시글)만 보려면 Viewer를 이용하면 된다.
// Toast UI Editor
import "@toast-ui/editor/dist/toastui-editor.css";
import '@toast-ui/editor/dist/toastui-editor-viewer.css'; // Viewer css
import { Editor, Viewer } from "@toast-ui/react-editor";
하지만 이 방법으로 Viewer를 불러도 ref가 동작하지 않아 setMarkdown이 적용되지 않는다.
<Viewer
ref={editorRef} // 미동작
initialValue={initContents}
usageStatistics={false}
plugins={[tableMergedCell]}
/>
따라서 Viewer를 new로 생성하는 방법을 이용한다.
아래와 같이 import 구문을 수정하자.
// Toast UI Editor
import "@toast-ui/editor/dist/toastui-editor.css";
import "@toast-ui/editor/dist/toastui-editor-viewer.css"; // Viewer css
import { Editor } from "@toast-ui/react-editor";
import Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';
useEffect에서 viewer를 생성하면 된다.
해당되는 el에 알맞은 toast-editor-viewr class를 가지는 div 태그를 추가해둬야 한다.
useEffect(() => {
let item = localStorage.getItem(CONTENT_KEY);
const viewer = new Viewer({
el: document.querySelector(".toast-editor-viewer"),
viewer: true,
height: "400px",
usageStatistics: false, // 통계 수집 거부
plugins:[tableMergedCell],
});
viewer.setMarkdown(item);
if (item) {
editorRef.current.getInstance().setMarkdown(item);
setInitContents(item);
}
}, []);
여기서는 Editor 위에 아래와 같이 추가하였다.
<div className="toast-editor-viewer"></div>
<Editor
ref={editorRef}
이제 에디터 위에 뷰어가 생성되었다.
이제 편집하기 버튼을 만들어 기본 설정으로는 Viewer를 보여주고, 편집 버튼을 누르면 Editor로 전환해 보자.
const [editMode, setEditMode] = useState(false);
편집하기 버튼의 클릭 이벤트에 setEditMode를 추가한다.
<Button
variant="outlined"
color="secondary"
sx={{ m: 1 }}
onClick={() => setEditMode(!editMode)}
>
{editMode ? "취소하기" : "편집하기"}
</Button>
editMode에 따라 Viewer와 Editor 렌더링을 구분한다.
{editMode === false && <div className="toast-editor-viewer"></div>}
{editMode === true && (
<Editor
ref={editorRef}
// initialValue={initContents}
height="400px"
placeholder="Please Enter Text."
previewStyle="tab" // or vertical
initialEditType="wysiwyg" // or markdown
// hideModeSwitch={true} // 하단 숨기기
toolbarItems={[
// 툴바 옵션 설정
["heading", "bold", "italic", "strike"],
["hr", "quote"],
["ul", "ol", "task", "indent", "outdent"],
["table", /* "image", */ "link"],
["code", "codeblock"],
]}
//theme="dark"
//useCommandShortcut={false} // 키보드 입력 컨트롤 방지 ex ctrl z 등
usageStatistics={false} // 통계 수집 거부
plugins={[[colorSyntax, colorSyntaxOptions], tableMergedCell]}
/>
)}
editMode에 따라 렌더링되는 태그가 달라지기 때문에 방어코드를 추가한다.
그리고 editMode가 변경될 때마다 useEffect가 실행되도록 [editMode]를 추가한다.
useEffect(() => {
let item = localStorage.getItem(CONTENT_KEY);
if(editMode === false) {
const viewer = new Viewer({
el: document.querySelector(".toast-editor-viewer"),
viewer: true,
height: "400px",
usageStatistics: false, // 통계 수집 거부
plugins: [tableMergedCell],
});
viewer.setMarkdown(item);
}
if (item) {
if(editorRef.current) editorRef.current.getInstance().setMarkdown(item);
}
}, [editMode]);
저장하기 버튼도 editMode인 경우에만 활성화되도록 disabled 옵션을 추가하자.
<Button
variant="outlined"
color="primary"
sx={{ m: 1 }}
onClick={handleSave}
disabled={editMode === false}
>
저장하기
</Button>
이제 게시글을 편집하고 저장하는 기능까지 완성되었다.
전체 코드는 다음과 같다.
import React, { useEffect, useRef, useState } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
// Toast UI Editor
import "@toast-ui/editor/dist/toastui-editor.css";
import "@toast-ui/editor/dist/toastui-editor-viewer.css"; // Viewer css
import { Editor } from "@toast-ui/react-editor";
import Viewer from "@toast-ui/editor/dist/toastui-editor-viewer";
// Dark Theme 적용
// import '@toast-ui/editor/dist/toastui-editor.css';
// import '@toast-ui/editor/dist/theme/toastui-editor-dark.css';
// Color Syntax Plugin
import "tui-color-picker/dist/tui-color-picker.css";
import "@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css";
import colorSyntax from "@toast-ui/editor-plugin-color-syntax";
// Table Merged Cell Plugin
import "@toast-ui/editor-plugin-table-merged-cell/dist/toastui-editor-plugin-table-merged-cell.css";
import tableMergedCell from "@toast-ui/editor-plugin-table-merged-cell";
const colorSyntaxOptions = {
preset: [
"#333333", "#666666", "#FFFFFF", "#EE2323", "#F89009", "#009A87", "#006DD7", "#8A3DB6",
"#781B33", "#5733B1", "#953B34", "#FFC1C8", "#FFC9AF", "#9FEEC3", "#99CEFA", "#C1BEF9",
],
};
const CONTENT_KEY = "CONTENT_KEY";
const App = () => {
const editorRef = useRef(null);
const [editMode, setEditMode] = useState(false);
const handleSave = () => {
let markDownContent = editorRef.current.getInstance().getMarkdown();
//let htmlContent = editorRef.current.getInstance().getHTML();
console.log(markDownContent);
localStorage.setItem(CONTENT_KEY, markDownContent);
};
useEffect(() => {
let item = localStorage.getItem(CONTENT_KEY);
if (editMode === false) {
const viewer = new Viewer({
el: document.querySelector(".toast-editor-viewer"),
viewer: true,
height: "400px",
usageStatistics: false, // 통계 수집 거부
plugins: [tableMergedCell],
});
viewer.setMarkdown(item);
}
if (item) {
if (editorRef.current) editorRef.current.getInstance().setMarkdown(item);
}
}, [editMode]);
return (
<div>
<Box sx={{ m: 2 }}>
<h1>Toast UI Editor</h1>
<Button
variant="outlined"
color="secondary"
sx={{ m: 1 }}
onClick={() => setEditMode(!editMode)}
>
{editMode ? "취소하기" : "편집하기"}
</Button>
<Button
variant="outlined"
color="primary"
sx={{ m: 1 }}
onClick={handleSave}
disabled={editMode === false}
>
저장하기
</Button>
{editMode === false && <div className="toast-editor-viewer"></div>}
{editMode === true && (
<Editor
ref={editorRef}
// initialValue={initContents}
height="400px"
placeholder="Please Enter Text."
previewStyle="tab" // or vertical
initialEditType="wysiwyg" // or markdown
// hideModeSwitch={true} // 하단 숨기기
toolbarItems={[
// 툴바 옵션 설정
["heading", "bold", "italic", "strike"],
["hr", "quote"],
["ul", "ol", "task", "indent", "outdent"],
["table", /* "image", */ "link"],
["code", "codeblock"],
]}
//theme="dark"
//useCommandShortcut={false} // 키보드 입력 컨트롤 방지 ex ctrl z 등
usageStatistics={false} // 통계 수집 거부
plugins={[[colorSyntax, colorSyntaxOptions], tableMergedCell]}
/>
)}
</Box>
</div>
);
};
export default App;
여기까지 구현 사항은 링크에서 확인해 보자.
깃허브에 저장하기
위에서 적용할 내용을 아래의 README.md에 저장해 보자.
https://github.com/bloodstrawberry/auto-test/blob/main/README.md
깃허브의 RESTful API로 파일을 읽고, 파일을 쓰는 코드를 추가하면 된다.
const getSHA = async (octokit) => {
const result = await octokit.request(
`GET /repos/bloodstrawberry/${repo}/contents/${path}`,
{
owner: "bloodstrawberry",
repo: `${repo}`,
path: `${path}`,
}
);
return result.data.sha;
};
const fileWrite = async (contents) => {
const octokit = new Octokit({
auth: myKey,
});
const currentSHA = await getSHA(octokit);
const result = await octokit.request(
`PUT /repos/bloodstrawberry/${repo}/contents/${path}`,
{
owner: "bloodstrawberry",
repo: `${repo}`,
path: `${path}`,
message: "commit message!",
sha: currentSHA,
committer: {
name: "bloodstrawberry",
email: "bloodstrawberry@github.com",
},
content: `${btoa(unescape(encodeURIComponent(`${contents}`)))}`,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
console.log(result.status);
};
const fileRead = async () => {
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 result;
};
위의 코드를 사용하기 위해 useEffect를 아래와 같이 수정한다.
const initReadMe = async () => {
// let item = localStorage.getItem(CONTENT_KEY);
let result = await fileRead();
let contents = decodeURIComponent(escape(window.atob(result.data.content)));
console.log(contents);
if (editMode === false) {
const viewer = new Viewer({
el: document.querySelector(".toast-editor-viewer"),
viewer: true,
height: "400px",
usageStatistics: false, // 통계 수집 거부
plugins: [tableMergedCell],
});
viewer.setMarkdown(contents);
}
if (editorRef.current)
editorRef.current.getInstance().setMarkdown(contents);
};
useEffect(() => {
initReadMe();
}, [editMode]);
handleSave에는 fileWrite를 추가한다.
const handleSave = () => {
let markDownContent = editorRef.current.getInstance().getMarkdown();
//let htmlContent = editorRef.current.getInstance().getHTML();
console.log(markDownContent);
//localStorage.setItem(CONTENT_KEY, markDownContent);
fileWrite(markDownContent);
};
정상적으로 깃허브에 반영되고 불러오는 것을 확인해 보자.
전체 코드는 다음과 같다.
import React, { useEffect, useRef, useState } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
// GitHub RESTful API
import { Octokit } from "@octokit/rest";
// Toast UI Editor
import "@toast-ui/editor/dist/toastui-editor.css";
import "@toast-ui/editor/dist/toastui-editor-viewer.css"; // Viewer css
import { Editor } from "@toast-ui/react-editor";
import Viewer from "@toast-ui/editor/dist/toastui-editor-viewer";
// Dark Theme 적용
// import '@toast-ui/editor/dist/toastui-editor.css';
// import '@toast-ui/editor/dist/theme/toastui-editor-dark.css';
// Color Syntax Plugin
import "tui-color-picker/dist/tui-color-picker.css";
import "@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css";
import colorSyntax from "@toast-ui/editor-plugin-color-syntax";
// Table Merged Cell Plugin
import "@toast-ui/editor-plugin-table-merged-cell/dist/toastui-editor-plugin-table-merged-cell.css";
import tableMergedCell from "@toast-ui/editor-plugin-table-merged-cell";
const colorSyntaxOptions = {
preset: [
"#333333", "#666666", "#FFFFFF", "#EE2323", "#F89009", "#009A87", "#006DD7", "#8A3DB6",
"#781B33", "#5733B1", "#953B34", "#FFC1C8", "#FFC9AF", "#9FEEC3", "#99CEFA", "#C1BEF9",
],
};
//const CONTENT_KEY = "CONTENT_KEY";
let myKey = "...";
const App = () => {
const editorRef = useRef(null);
const [editMode, setEditMode] = useState(false);
const repo = "auto-test";
const path = "README.md";
const getSHA = async (octokit) => {
const result = await octokit.request(
`GET /repos/bloodstrawberry/${repo}/contents/${path}`,
{
owner: "bloodstrawberry",
repo: `${repo}`,
path: `${path}`,
}
);
return result.data.sha;
};
const fileWrite = async (contents) => {
const octokit = new Octokit({
auth: myKey,
});
const currentSHA = await getSHA(octokit);
const result = await octokit.request(
`PUT /repos/bloodstrawberry/${repo}/contents/${path}`,
{
owner: "bloodstrawberry",
repo: `${repo}`,
path: `${path}`,
message: "commit message!",
sha: currentSHA,
committer: {
name: "bloodstrawberry",
email: "bloodstrawberry@github.com",
},
content: `${btoa(unescape(encodeURIComponent(`${contents}`)))}`,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
console.log(result.status);
};
const fileRead = async () => {
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 result;
};
const handleSave = () => {
let markDownContent = editorRef.current.getInstance().getMarkdown();
//let htmlContent = editorRef.current.getInstance().getHTML();
console.log(markDownContent);
//localStorage.setItem(CONTENT_KEY, markDownContent);
fileWrite(markDownContent);
};
const initReadMe = async () => {
// let item = localStorage.getItem(CONTENT_KEY);
let result = await fileRead();
let contents = decodeURIComponent(escape(window.atob(result.data.content)));
console.log(contents);
if (editMode === false) {
const viewer = new Viewer({
el: document.querySelector(".toast-editor-viewer"),
viewer: true,
height: "400px",
usageStatistics: false, // 통계 수집 거부
plugins: [tableMergedCell],
});
viewer.setMarkdown(contents);
}
if (editorRef.current)
editorRef.current.getInstance().setMarkdown(contents);
};
useEffect(() => {
initReadMe();
}, [editMode]);
return (
<div>
<Box sx={{ m: 2 }}>
<h1>Toast UI Editor</h1>
<Button
variant="outlined"
color="secondary"
sx={{ m: 1 }}
onClick={() => setEditMode(!editMode)}
>
{editMode ? "취소하기" : "편집하기"}
</Button>
<Button
variant="outlined"
color="primary"
sx={{ m: 1 }}
onClick={handleSave}
disabled={editMode === false}
>
저장하기
</Button>
{editMode === false && <div className="toast-editor-viewer"></div>}
{editMode === true && (
<Editor
ref={editorRef}
// initialValue={initContents}
height="400px"
placeholder="Please Enter Text."
previewStyle="tab" // or vertical
initialEditType="wysiwyg" // or markdown
// hideModeSwitch={true} // 하단 숨기기
toolbarItems={[
// 툴바 옵션 설정
["heading", "bold", "italic", "strike"],
["hr", "quote"],
["ul", "ol", "task", "indent", "outdent"],
["table", /* "image", */ "link"],
["code", "codeblock"],
]}
//theme="dark"
//useCommandShortcut={false} // 키보드 입력 컨트롤 방지 ex ctrl z 등
usageStatistics={false} // 통계 수집 거부
plugins={[[colorSyntax, colorSyntaxOptions], tableMergedCell]}
/>
)}
</Box>
</div>
);
};
export default App;
참고 : 최신 정보를 유지할 필요가 있다면 IndexedDB로 최신 정보를 저장하자.
'개발 > React' 카테고리의 다른 글
리액트 - Toast UI Editor with OAuth to Access GitHub (0) | 2023.08.19 |
---|---|
리액트 - .env 환경 변수 파일 관리하기 (0) | 2023.08.15 |
리액트 - Toast UI 에디터 저장하고 초기화하기 (Save and Initialize Toast UI Editor) (0) | 2023.07.30 |
리액트 - Toast UI 에디터 테이블 병합 플러그인 추가하기 (Add Table Merged Cell Plugin Toast UI Editor) (7) | 2023.07.30 |
리액트 - Toast UI 에디터 글자색 변경 플러그인 추가하기 (Add Color Syntax Plugin Toast UI Editor) (0) | 2023.07.30 |
댓글