본문 바로가기
개발/React

리액트 - GitHub RESTful API로 댓글 저장하기 with react-comments-section

by 피로물든딸기 2023. 11. 15.
반응형

리액트 전체 링크

Git / GitHub 전체 링크

 

참고

- https://www.npmjs.com/package/react-comments-section

- https://riyanegi.github.io/react-comments-documentation/

 

RESTful API로 파일 읽기
- RESTful API로 파일 쓰기

- RESTful API로 파일 삭제하기

 

댓글 기능 만들기 with react-comments-section
로그인한 사용자만 댓글 기능 사용하기
GitHub RESTful API로 댓글 저장하기
리액트 쿠키로 GitHub OAuth 로그인 인증 관리하기

- Mui Pagination으로 댓글 페이지로 나누기

 

깃허브 RESTful API를 이용하여 댓글을 저장해 보자.

 

react-comments-section에서는 댓글을 submit/delete/reply/edit할 때, 콜백 함수를 제공한다.

  const onSubmitAction = (data) => {
    console.log("check submit, ", data);
  };

  const onDeleteAction = (data) => {
    console.log("check Delete, ", data);
  };

  const onReplyAction = (data) => {
    console.log("check Reply, ", data);
  };

  const onEditAction = (data) => {
    console.log("check Edit, ", data);
  };

  const currentData = (data) => {
    console.log("current data", data);
  };

 

CommentSection에 추가해서 확인해 보자.

  <CommentSection
    currentUser={getCurrentUser}
    logIn={{
      loginLink: loginURL,
      signupLink: "https://github.com/",
    }}
    commentData={data}
    currentData={currentData}
    onSubmitAction={onSubmitAction}
    onDeleteAction={onDeleteAction}
    onReplyAction={onReplyAction}
    onEditAction={onEditAction}
    ...

 

각 함수가 정상적으로 호출된다.

하지만 data는 해당 user에 대한 정보만 가지고 있고, 전체 댓글 data는 current data에서 호출되고 있다.


Read

 

currentData에서 댓글을 저장할 수 있기 때문에 콜백 함수는 모두 주석 처리한다.

  // onSubmitAction={onSubmitAction}
  // onDeleteAction={onDeleteAction}
  // onReplyAction={onReplyAction}
  // onEditAction={onEditAction}

 

githublibrary.js에 fileRead 함수를 추가한다. (링크 참고)

const myKey = process.env.REACT_APP_MY_TOKEN;
const repo = `auto-test`; 

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 result;
  } catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};

 

initComments에는 깃허브 저장소에 댓글 정보가 있는 경우 useState를 이용하여 관리한다.

const [initComments, setInitComments] = useState([]);

  <CommentSection
    ...
    commentData={initComments}

 

loadComments 함수를 추가한다.

fileRead에 directory를 설정하면 해당 디렉토리에 존재하는 파일의 목록을 data에서 얻을 수 있다. 

댓글은 시간 순서대로 저장할 예정이므로, 해당 배열을 reverse하여 첫 번째 파일을 가져온다.

댓글이 존재하면 setInitComments로 설정한다.

  const loadComments = async() => {
    let commentDir = `comments`;
    let fileList = await gh.fileRead(commentDir);

    if(fileList === undefined) return;

    let latestFile = fileList.data.reverse()[0].name;
    let path = `comments/${latestFile}`;
    let result = await gh.fileRead(path);

    if(result === undefined) return;
    
    let contents = decodeURIComponent(escape(window.atob(result.data.content)));
    
    setInitComments(JSON.parse(contents));
  }
  
  useEffect(() => {
    loadComments();
  }, []);

 

예를 들어 아래와 같이 comments 폴더에 댓글 정보가 있다면

가장 나중에 저장된 2023_11_19_38_16_data.json이 설정된다.

 

위의 정보를 바탕으로 설정된 댓글은 아래와 같다.


Write

 

이제 댓글 파일을 저장하기 위한 시간 함수를 만든다.

const getCurrentDateTime = () => {
  const now = new Date();

  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  const hours = String(now.getHours()).padStart(2, "0");
  const minutes = String(now.getMinutes()).padStart(2, "0");
  const seconds = String(now.getSeconds()).padStart(2, "0");

  const formattedDateTime = `${year}_${month}_${day}_${hours}_${minutes}_${seconds}`;
  return formattedDateTime;
};

 

댓글이 변경되면 currentData가 호출되는데, 내부에서 setComments를 이용해 댓글을 관리하자.

const [comments, setComments] = useState([]);

 

data가 존재하지 않거나, 최초의 data와 같은 경우는 setComments를 호출하지 않는다.

  const currentData = (data) => {
    console.log("current data", data);
    if(data.length === 0 || data === initComments) return;
    setComments(data);
  };

 

comments가 변경될 때마다 saveComments를 실행한다.

하지만 최초로 로드될 때는 저장할 필요가 없기 때문에 useRef로 방어 코드를 추가하였다.

  const initMount = useRef(false);

  const saveComments = async () => {
    console.log("save Comments");

    let path = `comments/${getCurrentDateTime()}_data.json`;
    let result = await gh.fileCreate(JSON.stringify(comments, null, 2), path);

    console.log(result);
  };

  useEffect(() => {
    if (initMount.current) saveComments();
    else initMount.current = true;
  }, [comments]);

 

fileCreate는 githublibrary.js에 추가하였다. (링크 참고)

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

  try {
    const result = await octokit.request(
      `PUT /repos/bloodstrawberry/${repo}/contents/${path}`,
      {
        owner: "bloodstrawberry",
        repo: `${repo}`,
        path: `${path}`,
        message: "make comments",
        committer: {
          name: "bloodstrawberry",
          email: "bloodstrawberry@github.com",
        },
        content: `${btoa(unescape(encodeURIComponent(`${contents}`)))}`,
        headers: {
          "X-GitHub-Api-Version": "2022-11-28",
        },
      }
    );
    
    return result.status; 
  }
  catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};

 

댓글 상태가 매번 변할 때마다 파일이 추가되기 때문에 적절한 시점에 파일을 삭제하는 코드도 추가한다.

여기서는 생략하였다.

 

전체 코드는 다음과 같다.

 

ReactComments.js

import React, { useEffect, useRef, useState } from "react";
import { CommentSection } from "react-comments-section";
//import 'react-comments-section/dist/index.css';
import "../css/comment.css";
import * as gh from "../githublibrary.js";

const clientID = process.env.REACT_APP_CLIENT_ID;
const callbackURL = "http://localhost:3000/callback";
const loginURL = `https://github.com/login/oauth/authorize?client_id=${clientID}&scope=repo:status read:repo_hook user:email&redirect_uri=${callbackURL}`;

const getCurrentDateTime = () => {
  const now = new Date();

  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  const hours = String(now.getHours()).padStart(2, "0");
  const minutes = String(now.getMinutes()).padStart(2, "0");
  const seconds = String(now.getSeconds()).padStart(2, "0");

  const formattedDateTime = `${year}_${month}_${day}_${hours}_${minutes}_${seconds}`;
  return formattedDateTime;
};

const ReactComments = ({ currentUser }) => {
  const [initComments, setInitComments] = useState([]);
  const [comments, setComments] = useState([]);
  const initMount = useRef(false);

  const currentData = (data) => {
    console.log("current data", data);
    if(data.length === 0 || data === initComments) return;
    setComments(data);
  };

  const saveComments = async () => {
    console.log("save Comments");

    let path = `comments/${getCurrentDateTime()}_data.json`;
    let result = await gh.fileCreate(JSON.stringify(comments, null, 2), path);

    console.log(result);
  };

  useEffect(() => {
    if (initMount.current) saveComments();
    else initMount.current = true;
  }, [comments]); 
  
  const getCurrentUser = () => {
    if (currentUser === null) {
      let loginID = localStorage.getItem("LOGIN_ID");
      let url = localStorage.getItem("AVATAR_URL");
      let profile = `https://github.com/${loginID}`;

      if (loginID === null) return null;

      return {
        currentUserId: loginID,
        currentUserImg: url,
        currentUserProfile: profile,
        currentUserFullName: loginID,
      };
    }

    return currentUser;
  };

  const loadComments = async() => {
    let commentDir = `comments`;
    let fileList = await gh.fileRead(commentDir);

    if(fileList === undefined) return;

    let latestFile = fileList.data.reverse()[0].name;
    let path = `comments/${latestFile}`;
    let result = await gh.fileRead(path);

    if(result === undefined) return;
    
    let contents = decodeURIComponent(escape(window.atob(result.data.content)));
    
    setInitComments(JSON.parse(contents));
  }

  useEffect(() => {
    loadComments();
  }, []);

  return (
    <div>
      <CommentSection
        currentUser={getCurrentUser}
        logIn={{
          loginLink: loginURL,
          signupLink: "https://github.com/",
        }}
        commentData={initComments}
        currentData={currentData}
        // onSubmitAction={onSubmitAction}
        // onDeleteAction={onDeleteAction}
        // onReplyAction={onReplyAction}
        // onEditAction={onEditAction}
        hrStyle={{ border: "0.5px solid #ff0072" }}
        inputStyle={{ border: "1px solid rgb(208 208 208)" }}
        formStyle={{ backgroundColor: "white" }}
        submitBtnStyle={{
          border: "1px solid black",
          backgroundColor: "black",
          padding: "7px 15px",
        }}
        cancelBtnStyle={{
          border: "1px solid gray",
          backgroundColor: "gray",
          color: "white",
          padding: "7px 15px",
        }}
        replyInputStyle={{ borderBottom: "1px solid black", color: "black" }}
        advancedInput={true}
        removeEmoji={false}
      />
    </div>
  );
};

export default ReactComments;

 

githublibrary.js

import axios from "axios";
import { Octokit } from "@octokit/rest";

const myKey = process.env.REACT_APP_MY_TOKEN;
const repo = `auto-test`; 

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 result;
  } catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};

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

  try {
    const result = await octokit.request(
      `PUT /repos/bloodstrawberry/${repo}/contents/${path}`,
      {
        owner: "bloodstrawberry",
        repo: `${repo}`,
        path: `${path}`,
        message: "make comments",
        committer: {
          name: "bloodstrawberry",
          email: "bloodstrawberry@github.com",
        },
        content: `${btoa(unescape(encodeURIComponent(`${contents}`)))}`,
        headers: {
          "X-GitHub-Api-Version": "2022-11-28",
        },
      }
    );
    
    return result.status; 
  }
  catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};

export const loginCheck = async (setLoginStatus) => {
  let token = localStorage.getItem("GITHUB_TOKEN");
  try {
    const response = await axios.get("https://api.github.com/user", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    console.log(response);
    setLoginStatus(true);
  } catch (error) {
    console.error("Error fetching user data:", error);
    setLoginStatus(false);
  }
};

export const getLoginStatus = async () => {
  let token = localStorage.getItem("GITHUB_TOKEN");
  try {
    const response = await axios.get("https://api.github.com/user", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    console.log(response);
    return true;
  } catch (error) {
    console.error("Error fetching user data:", error);
    return false;
  }
};

실시간 댓글 저장하기

 

위의 방법대로라면 동시에 다른 사람이 댓글을 저장하는 경우, 실시간으로 올라오는 댓글이 저장되지 않을 수 있다.

따라서 원래 댓글에 submit / delete / reply / edit (= action)에 대한 기록을 남겨 댓글을 변경하도록 하자.


action data 분석

 

submit의 data는 아래와 같다.

{
  "userId": "bloodstrawberry",
  "comId": "e4e9aec2-7335-4a6b-9464-55b7da8a5b79",
  "avatarUrl": "https://avatars.githubusercontent.com/u/40860674?v=4",
  "userProfile": "https://github.com/bloodstrawberry",
  "fullName": "bloodstrawberry",
  "text": "<p>submit test</p>",
  "replies": []
}

 

delete의 data는 아래와 같다.

{
  "comIdToDelete": "d8c992fc-a242-442d-9cc4-10496721c690"
}

 

reply data는 아래와 같다.

{
  "userId": "bloodstrawberry",
  "repliedToCommentId": "017",
  "avatarUrl": "https://avatars.githubusercontent.com/u/40860674?v=4",
  "userProfile": "https://github.com/bloodstrawberry",
  "fullName": "bloodstrawberry",
  "text": "<p>reply test</p>",
  "comId": "ba95fa6d-3d1f-46af-8efd-e62a08278c08"
}

 

edit data는 아래와 같다.

{
  "userId": "bloodstrawberry",
  "comId": "d8c992fc-a242-442d-9cc4-10496721c690",
  "avatarUrl": "https://avatars.githubusercontent.com/u/40860674?v=4",
  "userProfile": "https://github.com/bloodstrawberry",
  "fullName": "bloodstrawberry",
  "text": "<p>edit test</p>"
}

 

reply의 경우, 댓글에 reply를 하는 경우와 reply에 reply를 할 때 추가되는 속성이 존재한다.

submit의 경우 parentOfRepliedCommentIdrepliedToCommentId가 없다.

submit

 

replyparentOfRepliedCommentIdundefined로 존재하고, repliedToCommentId가 존재한다.

reply

 

reply to replyparentOfRepliedCommentIdrepliedToCommentId가 모두 존재한다.

reply to reply

 

이제 리포지토리의 comments 폴더를 모두 지우고 구현해 보자.


저장 구현

 

saveCommentAction 함수를 아래와 같이 만들고 각 action에 추가하자.

그리고 나중에 경우를 나누기 쉽게 data의 type에 각 action 함수를 추가하였다.

  const saveCommentAction = async (data) => {
    console.log("save Comments Action");
    
    let path = `comments/${getCurrentDateTime()}_data.json`;
    let result = await gh.fileCreate(JSON.stringify(data, null, 2), path);

    console.log(result);
  };

  const onSubmitAction = (data) => {
    console.log("check submit, ", data);

    data.type = "submit";
    saveCommentAction(data);
  };

  const onDeleteAction = (data) => {
    console.log("check Delete, ", data);
    data.type = "delete";
    saveCommentAction(data);
  };

  const onReplyAction = (data) => {
    console.log("check Reply, ", data);
    data.type = "reply";
    
    saveCommentAction(data);
  };

  const onEditAction = (data) => {
    console.log("check Edit, ", data);

    data.type = "edit";
    saveCommentAction(data);
  };

 

그리고 컴포넌트에도 위의 메서드를 추가한다.

  <CommentSection
    ...
    commentData={initComments}
    currentData={currentData}
    onSubmitAction={onSubmitAction}
    onDeleteAction={onDeleteAction}
    onReplyAction={onReplyAction}
    onEditAction={onEditAction}

 

이제 아래의 동작을 실행해서 정상적으로 API가 동작하는지 확인해 보자.

 

1) submit : 1

2) submit : 2

3) submit : 3

4) reply 1 : A

5) reply 1 : B

6) reply 1 : C

7) reply 2 : AA

8) reply 2 : BB

9) reply 3 : AAA

10) edit : 1 → 1 edit

11) edit : 2 reply BB → BB edit

12) delete : 1 edit

13) delete : 2 reply BB

 

리포지토리에는 정상적으로 저장되었다면, 아래와 같이 파일을 확인할 수 있다.

 

파일은 아래와 같이 저장될 것이다.


댓글 불러오기

 

loadComments가 실행되면 아래와 같이 결과가 나와야한다.

 

comments 폴더에 아무 파일도 없다면 그대로 종료한다.

  const loadComments = async() => {
    let commentDir = `comments`;
    let fileList = await gh.fileRead(commentDir);

    if(fileList === undefined) return; // 아무 댓글도 없는 경우

 

위의 댓글에 action이 모두 실행되면 original_comments.js로 저장할 예정이다.

즉, original_comments.js가 존재하지 않는다면, 현재 댓글의 data는 []가 된다.

존재한다면 기존 코드대로 data를 읽어 originalComment에 저장한다.

    // original_comments.js 찾기
    let originalComment = fileList.data.filter((file) => file.name === "original_comments.js");    
    let originalCommentData = [];
    if(originalComment.length) {
      let result = await gh.fileRead(originalComment[0].path);
      if(result === undefined) return;
      
      originalCommentData = JSON.parse(decodeURIComponent(escape(window.atob(result.data.content))));
    }

 

original_comments.js를 제외한 모든 파일은 action 파일이므로 아래와 같이 필터링한다.

let commentsAction = fileList.data.filter((file) => file.name !== "original_comments.js");

 

action data는 시간 순으로 불러오기 때문에 for of로 순서대로 읽는다.

    for(let action of commentsAction) {
      console.log(action);
      let result = await gh.fileRead(action.path);
      if(result === undefined) {
        console.error("error data");
        continue;
      }

      let contents = JSON.parse(decodeURIComponent(escape(window.atob(result.data.content))));

 

해당 contents의 type에 따라 originalCommentData를 변경한다.

 

submit의 경우라면 data에 push를 하면 된다.

type은 필요 없기 때문에 삭제하였다.

  if(contents.type === "submit") {
    delete contents.type;
    originalCommentData.push(contents);
  }

 

삭제를 하는 경우 ID를 찾아서 삭제한다.

이때, 댓글을 통째로 삭제하는지, reply를 삭제하는지는 parentOfDeletedId로 구별할 수 있다.

  else if(contents.type === "delete") {
    let comIdToDelete = contents.comIdToDelete;
    let parentOfDeleteId = contents.parentOfDeleteId;

    if(parentOfDeleteId === undefined) { // 댓글 삭제
      originalCommentData = originalCommentData.filter((data) => data.comId !== comIdToDelete);
    } else { // reply 삭제
      let parent = originalCommentData.filter((data) => data.comId === parentOfDeleteId)[0];
      parent.replies = parent.replies.filter((data) => data.comId !== comIdToDelete);
    }
  }

 

reply의 경우, reply to reply라고 해도 댓글에 reply를 하는 것과 결과가 차이가 없다.

따라서 parentOfRepliedCommentId가 존재하지 않으면 repliedToCommentId가 reply 대상 댓글(부모)이 된다.

해당 ID를 찾아서 replies 배열에 push한다.

  else if(contents.type === "reply") {
    let repliedToCommentId = contents.parentOfRepliedCommentId || contents.repliedToCommentId;
    let parent = originalCommentData.filter((data) => data.comId === repliedToCommentId)[0];
    delete contents.type;
    parent.replies.push(contents);
  }

 

edit는 parentOfEditedCommentId에 따라 댓글을 변경한 건지, reply를 변경한 건지 알 수 있다.

  else if(contents.type === "edit") {
    let comId = contents.comId;
    let parentOfEditedCommentId = contents.parentOfEditedCommentId;

    if(parentOfEditedCommentId === undefined) { // 댓글을 변경한 경우
      let parentText = originalCommentData.filter((data) => data.comId === comId)[0];
      parentText.text = contents.text;
    } else { // reply를 변경한 경우
      let parent = originalCommentData.filter((data) => data.comId === parentOfEditedCommentId)[0];
      let child = parent.replies.filter((data) => data.comId === comId)[0];
      child.text = contents.text;
    }
  }

 

완성된 댓글 data를 setInitComments를 이용하여 상태를 변경한다.

setInitComments(originalCommentData);

 

이제 loadComments를 불러오면 정상적으로 댓글을 불러온다.


최적화

 

댓글의 action이 많을수록 불러야하는 데이터가 많기 때문에 위의 결과처럼 속도가 느려진다.

먼저 현재 데이터가 완성되면 original_comments.js로 저장한다.

  setInitComments(originalCommentData);

  let original = `comments/original_comments.js`;
  let result = await gh.fileCreate(JSON.stringify(original, null, 2), path);
  console.log(result);

 

그리고 기존에 있던 모든 action 파일을 지우기 위해 githublibrary.js에 delete API를 추가한다.

(file을 삭제하기 위해서는 SHA가 필요하다.)

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

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

  catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};

export const fileDelete = async (path) => {
  let sha = await getSHA(path);
  if(sha === undefined) return undefined;
  try {
    const octokit = new Octokit({
      auth: myKey,
    });

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

    return result;
  } catch (e) {
    console.log("hello!!");
    console.log("error : ", e);
    return undefined;
  }
};

 

action 파일이 존재하는 경우, original data가 존재하면 지우고 다시 original data를 만든다.

  setInitComments(originalCommentData);

  if (commentsAction.length === 0) return;

  // 기존 data는 삭제
  let originalPath = `comments/original_comments.js`;
  let result = await gh.fileDelete(originalPath);

  console.log(`${originalPath} delete : ${result}`);

  // 새로 생성
  result = await gh.fileCreate(
    JSON.stringify(originalCommentData, null, 2),
    originalPath
  );

  console.log(result);

 

위 API를 이용하여 action 파일을 모두 지운다. (파일을 한꺼번에 지우는 API는 아직 제공되지 않는다.)

  for(let action of commentsAction) {
    result = await gh.fileDelete(action.path);
    console.log(`${action.path} delete : ${result}`);
  }

 

정상적으로 action 파일이 삭제되는 것을 알 수 있다.

 

그리고 original_comments.js만 남게 된다.

 

다시 댓글을 load하면 original_comments만 읽어서 댓글이 완성되고, 매번 action을 read하지 않아도 된다.

이때, 깃허브는 실시간 DB가 아니기 때문에 1분 정도의 Delay는 발생한다.


전체 코드는 다음과 같다.

 

ReactComments.js

import React, { useEffect, useState } from "react";
import { CommentSection } from "react-comments-section";
//import 'react-comments-section/dist/index.css';
import "../css/comment.css";
import * as gh from "../githublibrary.js";

const clientID = process.env.REACT_APP_CLIENT_ID;
const callbackURL = "http://localhost:3000/callback";
const loginURL = `https://github.com/login/oauth/authorize?client_id=${clientID}&scope=repo:status read:repo_hook user:email&redirect_uri=${callbackURL}`;

const getCurrentDateTime = () => {
  const now = new Date();

  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  const hours = String(now.getHours()).padStart(2, "0");
  const minutes = String(now.getMinutes()).padStart(2, "0");
  const seconds = String(now.getSeconds()).padStart(2, "0");

  const formattedDateTime = `${year}_${month}_${day}_${hours}_${minutes}_${seconds}`;
  return formattedDateTime;
};

const ReactComments = ({ currentUser }) => {
  const [initComments, setInitComments] = useState([]);

  const currentData = (data) => {
    console.log("current data", data);
  };

  const getCurrentUser = () => {
    if (currentUser === null) {
      let loginID = localStorage.getItem("LOGIN_ID");
      let url = localStorage.getItem("AVATAR_URL");
      let profile = `https://github.com/${loginID}`;

      if (loginID === null) return null;

      return {
        currentUserId: loginID,
        currentUserImg: url,
        currentUserProfile: profile,
        currentUserFullName: loginID,
      };
    }

    return currentUser;
  };

  const loadComments = async () => {
    let commentDir = `comments`;
    let fileList = await gh.fileRead(commentDir);

    if (fileList === undefined) return; // 아무 댓글도 없는 경우

    // original_comments.js 찾기
    let originalComment = fileList.data.filter(
      (file) => file.name === "original_comments.js"
    );
    let originalCommentData = [];
    if (originalComment.length) {
      let result = await gh.fileRead(originalComment[0].path);
      if (result === undefined) return;

      originalCommentData = JSON.parse(
        decodeURIComponent(escape(window.atob(result.data.content)))
      );
    }

    let commentsAction = fileList.data.filter(
      (file) => file.name !== "original_comments.js"
    );

    for (let action of commentsAction) {
      let result = await gh.fileRead(action.path);

      if (result === undefined) {
        console.error("error data");
        continue;
      }

      //console.log(result.data.name);
      let contents = JSON.parse(
        decodeURIComponent(escape(window.atob(result.data.content)))
      );

      //console.log(contents);
      if (contents.type === "submit") {
        delete contents.type;
        originalCommentData.push(contents);
      } else if (contents.type === "delete") {
        let comIdToDelete = contents.comIdToDelete;
        let parentOfDeleteId = contents.parentOfDeleteId;

        if (parentOfDeleteId === undefined) {
          // 댓글 삭제
          originalCommentData = originalCommentData.filter(
            (data) => data.comId !== comIdToDelete
          );
        } else {
          // reply 삭제
          let parent = originalCommentData.filter(
            (data) => data.comId === parentOfDeleteId
          )[0];
          parent.replies = parent.replies.filter(
            (data) => data.comId !== comIdToDelete
          );
        }
      } else if (contents.type === "reply") {
        let repliedToCommentId =
          contents.parentOfRepliedCommentId || contents.repliedToCommentId;
        let parent = originalCommentData.filter(
          (data) => data.comId === repliedToCommentId
        )[0];
        delete contents.type;
        parent.replies.push(contents);
      } else if (contents.type === "edit") {
        let comId = contents.comId;
        let parentOfEditedCommentId = contents.parentOfEditedCommentId;

        if (parentOfEditedCommentId === undefined) {
          // 댓글을 변경한 경우
          let parentText = originalCommentData.filter(
            (data) => data.comId === comId
          )[0];
          parentText.text = contents.text;
        } else {
          // reply를 변경한 경우
          let parent = originalCommentData.filter(
            (data) => data.comId === parentOfEditedCommentId
          )[0];
          let child = parent.replies.filter((data) => data.comId === comId)[0];
          child.text = contents.text;
        }
      }
    }

    setInitComments(originalCommentData);

    if (commentsAction.length === 0) return;

    // 기존 data는 삭제
    let originalPath = `comments/original_comments.js`;
    let result = await gh.fileDelete(originalPath);

    console.log(`${originalPath} delete : ${result}`);

    // 새로 생성
    result = await gh.fileCreate(
      JSON.stringify(originalCommentData, null, 2),
      originalPath
    );

    console.log(result);

    // 불필요한 action 삭제
    for (let action of commentsAction) {
      result = await gh.fileDelete(action.path);
      console.log(`${action.path} delete : ${result}`);
    }
  };

  useEffect(() => {
    loadComments();
  }, []);

  const saveCommentAction = async (data) => {
    console.log("save Comments Action");

    let path = `comments/${getCurrentDateTime()}_data.json`;
    let result = await gh.fileCreate(JSON.stringify(data, null, 2), path);

    console.log(result);
  };

  const onSubmitAction = (data) => {
    console.log("check submit, ", data);

    data.type = "submit";
    saveCommentAction(data);
  };

  const onDeleteAction = (data) => {
    console.log("check Delete, ", data);
    data.type = "delete";
    saveCommentAction(data);
  };

  const onReplyAction = (data) => {
    console.log("check Reply, ", data);
    data.type = "reply";

    saveCommentAction(data);
  };

  const onEditAction = (data) => {
    console.log("check Edit, ", data);

    data.type = "edit";
    saveCommentAction(data);
  };

  return (
    <div>
      <CommentSection
        currentUser={getCurrentUser}
        logIn={{
          loginLink: loginURL,
          signupLink: "https://github.com/",
        }}
        commentData={initComments}
        currentData={currentData}
        onSubmitAction={onSubmitAction}
        onDeleteAction={onDeleteAction}
        onReplyAction={onReplyAction}
        onEditAction={onEditAction}
        hrStyle={{ border: "0.5px solid #ff0072" }}
        inputStyle={{ border: "1px solid rgb(208 208 208)" }}
        formStyle={{ backgroundColor: "white" }}
        submitBtnStyle={{
          border: "1px solid black",
          backgroundColor: "black",
          padding: "7px 15px",
        }}
        cancelBtnStyle={{
          border: "1px solid gray",
          backgroundColor: "gray",
          color: "white",
          padding: "7px 15px",
        }}
        replyInputStyle={{ borderBottom: "1px solid black", color: "black" }}
        advancedInput={true}
        removeEmoji={false}
      />
    </div>
  );
};

export default ReactComments;

 

githublibrary.js

import axios from "axios";
import { Octokit } from "@octokit/rest";

import * as ck from "./cookielibrary.js";

const myKey = process.env.REACT_APP_MY_TOKEN;
const repo = `auto-test`;

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

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

  catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};

export const fileDelete = async (path) => {
  let sha = await getSHA(path);
  if(sha === undefined) return undefined;
  try {
    const octokit = new Octokit({
      auth: myKey,
    });

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

    return result;
  } catch (e) {
    console.log("hello!!");
    console.log("error : ", e);
    return undefined;
  }
};

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 result;
  } catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};

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

  try {
    const result = await octokit.request(
      `PUT /repos/bloodstrawberry/${repo}/contents/${path}`,
      {
        owner: "bloodstrawberry",
        repo: `${repo}`,
        path: `${path}`,
        message: "make comments",
        committer: {
          name: "bloodstrawberry",
          email: "bloodstrawberry@github.com",
        },
        content: `${btoa(unescape(encodeURIComponent(`${contents}`)))}`,
        headers: {
          "X-GitHub-Api-Version": "2022-11-28",
        },
      }
    );

    return result.status;
  } catch (e) {
    console.log("error : ", e);
    return undefined;
  }
};

export const loginCheck = async (setLoginStatus) => {
  let token = ck.getCookies("GITHUB_TOKEN");
  try {
    const response = await axios.get("https://api.github.com/user", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    console.log(response);
    setLoginStatus(true);
  } catch (error) {
    console.error("Error fetching user data:", error);
    setLoginStatus(false);
  }
};

export const getLoginStatus = async () => {
  let token = ck.getCookies("GITHUB_TOKEN");
  try {
    const response = await axios.get("https://api.github.com/user", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    console.log(response);
    return true;
  } catch (error) {
    console.error("Error fetching user data:", error);
    return false;
  }
};
반응형

댓글