본문 바로가기
개발/React

리액트 - Toast UI Editor with OAuth to Access GitHub

by 피로물든딸기 2023. 8. 19.
반응형

깃허브 데스크탑으로 프로젝트 관리하기 강의 오픈!! (인프런 바로가기)

 

프로젝트 전체 링크

리액트 전체 링크

Git / GitHub 전체 링크

 

참고

- 깃허브 OAuth Access 토큰 발급 받기

- Toast UI 에디터로 깃허브 마크다운 저장하기

 

GitHub OAuth Project Settings
Material UI로 깃허브 로그인 프로필 만들기
깃허브 OAuth 콜백 처리하기
인증 토큰 획득 서버 구현하기
인증 토큰으로 로그인 상태 관리하기
로그인 정보를 활용하여 Commit Message 남기기
새 창으로 로그인해서 현재 상태 유지하기

 

Toast UI 에디터에서 깃허브 마크다운을 저장할 때, 

RESTful API를 사용하기 위해 keyowner를 고정시켰다.

따라서 해당 서비스가 배포되면, 누가 파일을 수정하고 있는지 알 수 없다.

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

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

    return result;
  };

 

리액트에 GitHub OAuth를 이용해 GitHub에 인증된 사용자만 편집하고, user ID를 commit 메시지에 남겨보자.


Project Settings

 

Toast UI Editor를 포함하여 아래의 코드에서 시작한다.

 

App.js

import React from "react";
import { Route, Link, Routes } from "react-router-dom";

import Home from "./page/Home";
import SimpleToastEditor from "./page/SimpleToastEditor";

import './App.css';

const App = () => {
  return (
    <div>
      <div className="router">
        <span>
          <Link to="/">Home</Link>
        </span>
        <span>
          <Link to="/editor">Toast UI Editor</Link>
        </span>
      </div>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/editor" element={<SimpleToastEditor />} />
      </Routes>
    </div>
  );
};

export default App;

 

page/Home.js

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

const Home = () => {
  return (
    <div>
      <Box sx={{ m: 2 }}>
        <h1>GitHub OAuth Test</h1>
      </Box>
    </div>
  );
};

export default Home;

 

page/SimpleToastEditor.js

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 = process.env.REACT_APP_MY_TOKEN;

const SimpleToastEditor = () => {
  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 SimpleToastEditor;

 

App.css

.router > span {
    margin: 10px;
}

.router {
    margin-bottom: 20px;
}

 

아래와 같은 화면이 나오면 Project Settings가 완성된다.

반응형

댓글