본문 바로가기
개발/React

리액트 - 파일 편집 후 메일 알림이 가도록 수정하기 with GitHub RESTful API

by 피로물든딸기 2023. 9. 2.
반응형

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

 

리액트 전체 링크

Git / GitHub 전체 링크

 

참고

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

- RESTful API로 브랜치 SHA 구하기

- RESTful API로 브랜치 만들고 삭제하기

- RESTful API로 PR 만들고 병합하기

- 메일 알림 설정하기

 

아래의 토스트 UI 에디터는 메인에서 직접 commit하기 때문에 code owners에게 메일 알림이 가지 않는다.

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;

 

따라서 fileWrite의 flow를 수정한다.

 

최신 브랜치 생성fileWritePR 생성 및 Merge브랜치 삭제 

 

브랜치 생성, 삭제 함수는 다음과 같다.

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

    const currentSHA = await getSHAforMain(octokit);
    console.log(currentSHA);
    const result = await octokit.git.createRef({
      owner: "bloodstrawberry",
      repo: `${repo}`,
      ref: `refs/heads/${branchName}`, // 새로운 브랜치 이름
      sha: currentSHA, // 기반 커밋의 SHA
    });

    console.log(result);
    return result;
  };

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

    const result = await octokit.git.deleteRef({
      owner: "bloodstrawberry",
      repo: `${repo}`,
      ref: `heads/${branchName}`, // 새로운 브랜치 이름
    });

    console.log("delete!!", result);
  };

 

PR 요청 및 Merge는 다음과 같다.

  const createPullRequest = async (branchName, title, body) => {
    const octokit = new Octokit({
      auth: myKey,
    });

    const result = await octokit.pulls.create({
      owner: "bloodstrawberry",
      repo: `${repo}`,
      title,
      body,
      head: branchName, // 현재 브랜치
      base: "main",
    });

    console.log(result);
    console.log("Pull Request Created:", result.data);

    const mergeResult = await octokit.pulls.merge({
      owner: "bloodstrawberry",
      repo: `${repo}`,
      pull_number: result.data.number, // 생성된 PR의 번호를 사용
    });

    console.log(mergeResult);
    console.log("Pull Request Merged:", mergeResult.data);

    return mergeResult;
  };

 

fileWrite는 flow대로 수정하면 된다.

여기서는 브랜치 이름을 test_branch로 명시하였다.

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

    // 브랜치 생성
    const branchName = "test_branch";
    const createResult = await makeBranch(branchName);
    console.log(createResult);

    // const currentSHA = await getSHA(octokit);
    // 브랜치 파일에 대한 SHA 획득
    const currentSHA = await getSHAforBranchFile(octokit, branchName);
    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}`)))}`,
        branch: branchName, // 해당 branch의 파일을 수정
        headers: {
          "X-GitHub-Api-Version": "2022-11-28",
        },
      }
    );

    console.log(result.status);

    // PR + Merge
    const prResult = await createPullRequest(branchName, "PR TITLE", "PR TEST");
    console.log(prResult);

    // 브랜치 삭제
    deleteBranch(branchName);
  };

 

각 함수에 대해 SHA를 구하는 방법링크 또는 전체 코드를 참고하자.

 

fileWrite에서 생성한 브랜치의 이름이 "test_branch"이기 때문에 commit에도 남아있는 것을 알 수 있다.

그리고 CODEOWNERS에 등록된 계정에 메일도 정상적으로 도착하였다.

 

전체 코드는 다음과 같다.

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 App = () => {
  const editorRef = useRef(null);
  const [editMode, setEditMode] = useState(false);
  const repo = "auto-test";
  const path = "README.md";

  const getSHAforMain = async (octokit) => {
    const response = await octokit.git.getRef({
      owner: "bloodstrawberry",
      repo: `${repo}`,
      ref: "heads/main", // main 브랜치의 이름
    });

    return response.data.object.sha;
  };

  const getSHAforBranchFile = async (octokit, branchName) => {
    const result = await octokit.request(
      `GET /repos/bloodstrawberry/${repo}/contents/${path}`,
      {
        owner: "bloodstrawberry",
        repo: `${repo}`,
        path: `${path}`,
        ref: branchName, // 브랜치 이름을 ref에 지정
      }
    );

    return result.data.sha;
  };

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

    const currentSHA = await getSHAforMain(octokit);
    console.log(currentSHA);
    const result = await octokit.git.createRef({
      owner: "bloodstrawberry",
      repo: `${repo}`,
      ref: `refs/heads/${branchName}`, // 새로운 브랜치 이름
      sha: currentSHA, // 기반 커밋의 SHA
    });

    console.log(result);
    return result;
  };

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

    const result = await octokit.git.deleteRef({
      owner: "bloodstrawberry",
      repo: `${repo}`,
      ref: `heads/${branchName}`, // 새로운 브랜치 이름
    });

    console.log("delete!!", result);
  };

  const createPullRequest = async (branchName, title, body) => {
    const octokit = new Octokit({
      auth: myKey,
    });

    const result = await octokit.pulls.create({
      owner: "bloodstrawberry",
      repo: `${repo}`,
      title,
      body,
      head: branchName, // 현재 브랜치
      base: "main",
    });

    console.log(result);
    console.log("Pull Request Created:", result.data);

    const mergeResult = await octokit.pulls.merge({
      owner: "bloodstrawberry",
      repo: `${repo}`,
      pull_number: result.data.number, // 생성된 PR의 번호를 사용
    });

    console.log(mergeResult);
    console.log("Pull Request Merged:", mergeResult.data);

    return mergeResult;
  };

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

    // 브랜치 생성
    const branchName = "test_branch";
    const createResult = await makeBranch(branchName);
    console.log(createResult);

    // const currentSHA = await getSHA(octokit);
    // 브랜치 파일에 대한 SHA 획득
    const currentSHA = await getSHAforBranchFile(octokit, branchName);
    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}`)))}`,
        branch: branchName, // 해당 branch의 파일을 수정
        headers: {
          "X-GitHub-Api-Version": "2022-11-28",
        },
      }
    );

    console.log(result.status);

    // PR + Merge
    const prResult = await createPullRequest(branchName, "PR TITLE", "PR TEST");
    console.log(prResult);

    // 브랜치 삭제
    deleteBranch(branchName);
  };

  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;
반응형

댓글