본문 바로가기
개발/React

리액트 - Handsontable 깃허브 연동하기 (data, style, comment, merge 저장하기)

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

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

 

리액트 전체 링크

Git / GitHub 전체 링크

 

참고

- .env 환경 변수 파일 관리하기

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

 

- Project Settings (전체 코드)

- True / False Options

- Selected Options

- Number Options

- width, height, placeholder, sort

- 주석, comment, memo, tooltip

- Merge Cells, 셀 합치기

- Search 구현

- Columns Data Type

- Cell 커스터마이징

- afterSelection으로 수식 입력줄 구현하기

- Download CSV 구현 (콤마, 줄바꿈, 따옴표 처리)

- Mui Drawer로 Handsontable Option 관리하기

- Column Width, Row Height 로컬 스토리지에 저장하기

- Mui 토글 버튼으로 셀 스타일 편집 기능 만들기 

- 셀 스타일 로컬 스토리지에 저장하기 (전체 코드)

- Handsontable 깃허브 연동하기 (data, style, comment, merge 저장하기)


깃허브 RESTful API를 이용하여 깃허브 저장하기 버튼을 구현해 보자.

 

토큰은 .env에 임시로 저장하고 npm run build를 실행한다. 

(실제 토큰은 서버를 이용해 획득해야 한다.)

REACT_APP_MY_TOKEN=ghp_g...I6zB

 

아래와 같이 SaveButtonGitHub.js를 추가한다.

import React from "react";

import Button from "@mui/material/Button";

let myKey = process.env.REACT_APP_MY_TOKEN;

const SaveButtonGitHub = ({ myHandsOnTable }) => {
  const saveGitHub = () => {};

  return (
    <Button
      sx={{ m: 2 }}
      variant="outlined"
      color="warning"
      onClick={saveGitHub}
    >
      깃허브 저장하기
    </Button>
  );
};

export default SaveButtonGitHub;

 

테이블을 생성하는 곳에서 적절한 위치에 SaveButtonGitHub를 추가하고 props로 myHandsOnTable을 추가한다.

  <div>
    <Box sx={{ m: 2 }}>
      <Button>
        Download CSV
      </Button>
      
      <SaveButtonGitHub myHandsOnTable={myHandsOnTable} />

      ...
      <div id="hot-app" style={{ marginTop: "13px" }}></div>
    </Box>
  </div>

로컬 스토리지 정리하기

 

먼저 지금까지 따로 자동 저장하는 로컬 스토리지를, 저장 버튼을 누르면 되도록 코드를 정리하자.

 

saveGitHub 메서드에서 현재 table의 데이터와 그 외 정보를 아래와 같이 한 번에 저장할 수 있다.

기존에 자동으로 저장시킨 로컬 스토리지 코드는 지운다. (전체 코드 참고)

  const ALL_DATA_KEY = "ALL_DATA_KEY";
  
  ...
  
  const saveGitHub = async () => {
    let data = myHandsOnTable.getData();
    let cellStyle = getEmptyArray();
    let row = cellStyle.length;
    let col = cellStyle[0].length;
    for (let r = 0; r < row; r++) {
      for (let c = 0; c < col; c++) {
        let cellInfo = myHandsOnTable.getCell(r, c);
        cellStyle[r][c].className = cellInfo.className;
        cellStyle[r][c].style.fontWeight = cellInfo.style.fontWeight;
        cellStyle[r][c].style.fontStyle = cellInfo.style.fontStyle;
        cellStyle[r][c].style.textDecoration = cellInfo.style.textDecoration;
        cellStyle[r][c].style.color = cellInfo.style.color;
        cellStyle[r][c].style.backgroundColor = cellInfo.style.backgroundColor;
      }
    }

    let comments = [];
    let mergeCells = [];
    for (let r = 0; r < row; r++) {
      for (let c = 0; c < col; c++) {
        let cellMeta = myHandsOnTable.getCellMeta(r, c);
        //console.log(cellMeta);
        if (cellMeta.comment)
          comments.push({
            row: r,
            col: c,
            comment: { value: cellMeta.comment.value },
          });

        if (cellMeta.spanned) {
          let rowspan = myHandsOnTable.getCell(r, c).rowSpan;
          let colspan = myHandsOnTable.getCell(r, c).colSpan;

          mergeCells.push({ row: r, col: c, rowspan, colspan });
        }
      }
    }

    let json = { data, cellStyle, comments, mergeCells };
    localStorage.setItem(ALL_DATA_KEY, JSON.stringify(json));
  };

 

cell과 mergeCells는 빈 배열을 추가하고, cells에서 사용한 변수는 cellStyle로 이름을 변경한다.

  const ALL_DATA_KEY = "ALL_DATA_KEY";
  let cellStyle = null;
  
  ...
  
  const options = {
    data, // initData(),

    ...

    comments: true /* 주석, 메모 기능 context menu에 추가 */,
    
    cell: [],
    mergeCells: [],

    cells: function(row, col, prop) {
      if (
        cellStyle === null ||
        cellStyle[row] === undefined || // insert 방어 코드
        cellStyle[row][col] === undefined
      )
        return {};

      let cellProperties = {};

      cellProperties.className =
        cellStyle[row][col].className || "htCenter htMiddle"; // undefined 처리

      cellProperties.renderer = function(instance, td) {
        Handsontable.renderers.TextRenderer.apply(this, arguments);
        td.style.fontWeight = cellStyle[row][col].style.fontWeight || "";
        td.style.fontStyle = cellStyle[row][col].style.fontStyle || "";
        td.style.textDecoration =
          cellStyle[row][col].style.textDecoration || "";
        td.style.color = cellStyle[row][col].style.color || "#000000";
        td.style.backgroundColor =
          cellStyle[row][col].style.backgroundColor || "#FFFFFF";
      };

      return cellProperties;
    },

    licenseKey: "non-commercial-and-evaluation",
  };

 

이제 table을 초기화하기 전에 localStorage에 있는 값을 보고 options를 변경한다.

  let myTable;
  const makeTable = () => {
    let allData = localStorage.getItem(ALL_DATA_KEY);
    if (allData !== null) {
      allData = JSON.parse(allData);
      options.data = allData.data;
      options.cell = allData.comments;
      options.mergeCells = allData.mergeCells;
      cellStyle = allData.cellStyle;
    }

    const container = document.getElementById("hot-app");
    container.innerHTML = "";

    myTable = new Handsontable(container, {
      ...options,
      ...myOptions.trueFalseOptions,
      ...myOptions.numberOptions,
      ...myOptions.cellInfo,
    });
    
    ...
    
  }

 

저장하기 버튼을 누르면 새로 고침을 해도 테이블이 그대로 유지된다.


깃허브 RESTful API 구현하기

 

API 구현은 Toast UI 에디터로 깃허브 마크다운 저장하기에 있는 코드를 가져다가 사용한다.

 

로컬 스토리지에 저장하는 코드를 지우고, 깃허브 파일에 저장하도록 코드를 수정하였다.

  let json = { data, cellStyle, comments, mergeCells };
  // localStorage.setItem(ALL_DATA_KEY, JSON.stringify(json));

  let checkFileExist = await fileExist();
  if (checkFileExist) fileWrite(JSON.stringify(json, null, 2));
  else fileCreate(JSON.stringify(json, null, 2));

 

파일이 없다면 파일을 생성하는 fileCreate 코드를 추가하였다.

  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 fileExist = async () => {
    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 true;
    } catch (e) {
      return false;
    }
  };

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

    const result = await octokit.request(
      `PUT /repos/bloodstrawberry/${repo}/contents/${path}`,
      {
        owner: "bloodstrawberry",
        repo: `${repo}`,
        path: `${path}`,
        message: "commit message!",
        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 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);
  };

 

마지막으로 CustomHansOnTable.js에 깃허브에 있는 파일을 읽어오는 코드를 추가한다.

  let myTable;
  const makeTable = async () => {
    // let allData = localStorage.getItem(ALL_DATA_KEY);
    // if (allData !== null) {
    //   allData = JSON.parse(allData);
    //   options.data = allData.data;
    //   options.cell = allData.comments;
    //   options.mergeCells = allData.mergeCells;
    //   cellStyle = allData.cellStyle;
    // }

    let allData = await fileRead();
    if (allData !== undefined) {
      allData = JSON.parse(allData);
      options.data = allData.data;
      options.cell = allData.comments;
      options.mergeCells = allData.mergeCells;
      cellStyle = allData.cellStyle;
    }
    
    ...

 

테이블에 row나 col을 삭제 / 추가하고 그 외에도 적절히 변경해보자.

 

저장하기 버튼을 누르면 repo handsontable.json이 저장되는 것을 알 수 있다.

 

위의 테이블은 깃허브에 아래와 같이 저장된다.

{
  "data": [
    [
      "",
      "2017",
      "2018",
      "2019",
      "1",
      "2020",
      "2021",
      "2022"
    ],
    [
      "Tesla",
      null,
      null,
      5,
      "2",
      10,
      14,
      5
    ],
    [
      null,
      null,
      null,
      10,
      "3",
      12,
      12,
      null
    ],
    [
      "Honda",
      5,
      3,
      7,
      "4",
      13,
      null,
      null
    ],
    [
      "Mazda",
      4,
      7,
      5,
      "5",
      14,
      10,
      4
    ]
  ],
  "cellStyle": [
    [
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "underline line-through",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "underline line-through",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      }
    ],
    [
      {
        "className": "htMiddle htCenter htNoWrap current area fullySelectedMergedCell-0 highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap current area fullySelectedMergedCell-0 highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap current area fullySelectedMergedCell-0 highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap area highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap area highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap htCommentCell",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(244, 78, 59)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      }
    ],
    [
      {
        "className": "htMiddle htCenter htNoWrap current area fullySelectedMergedCell-0 highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap current area fullySelectedMergedCell-0 highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap current area fullySelectedMergedCell-0 highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap area highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap area highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(244, 78, 59)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      }
    ],
    [
      {
        "className": "htMiddle htCenter htNoWrap area highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap area highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap area highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap area highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap area highlight",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(128, 137, 0)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(244, 78, 59)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      }
    ],
    [
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "bold",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "bold",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "bold",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      },
      {
        "className": "htMiddle htCenter htNoWrap",
        "style": {
          "fontWeight": "",
          "fontStyle": "",
          "textDecoration": "",
          "color": "rgb(0, 0, 0)",
          "backgroundColor": "rgb(255, 255, 255)"
        }
      }
    ]
  ],
  "comments": [
    {
      "row": 1,
      "col": 5,
      "comment": {
        "value": "ㅋㅋㅋㅋ"
      }
    }
  ],
  "mergeCells": [
    {
      "row": 1,
      "col": 0,
      "rowspan": 2,
      "colspan": 3
    },
    {
      "row": 2,
      "col": 6,
      "rowspan": 2,
      "colspan": 2
    }
  ]
}

전체 코드

 

SaveButtonGitHub.js

import React from "react";

import Button from "@mui/material/Button";

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

let myKey = process.env.REACT_APP_MY_TOKEN;

const repo = `auto-test`; // my repository
const path = `handsontable.json`; // fileName;

const SaveButtonGitHub = ({ myHandsOnTable }) => {
  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 fileExist = async () => {
    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 true;
    } catch (e) {
      return false;
    }
  };

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

    const result = await octokit.request(
      `PUT /repos/bloodstrawberry/${repo}/contents/${path}`,
      {
        owner: "bloodstrawberry",
        repo: `${repo}`,
        path: `${path}`,
        message: "commit message!",
        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 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 getEmptyArray = () => {
    let row = myHandsOnTable.getData().length;
    let col = myHandsOnTable.getData()[0].length;
    let emptyArray = [];

    for (let r = 0; r < row; r++) {
      emptyArray[r] = [];
      for (let c = 0; c < col; c++)
        emptyArray[r][c] = {
          className: undefined,
          style: {
            fontWeight: undefined,
            fontStyle: undefined,
            textDecoration: undefined,
            color: undefined,
            backgroundColor: undefined,
          },
        };
    }
    return emptyArray;
  };

  const saveGitHub = async () => {
    let data = myHandsOnTable.getData();
    let cellStyle = getEmptyArray();
    let row = cellStyle.length;
    let col = cellStyle[0].length;
    for (let r = 0; r < row; r++) {
      for (let c = 0; c < col; c++) {
        let cellInfo = myHandsOnTable.getCell(r, c);
        cellStyle[r][c].className = cellInfo.className;
        cellStyle[r][c].style.fontWeight = cellInfo.style.fontWeight;
        cellStyle[r][c].style.fontStyle = cellInfo.style.fontStyle;
        cellStyle[r][c].style.textDecoration = cellInfo.style.textDecoration;
        cellStyle[r][c].style.color = cellInfo.style.color;
        cellStyle[r][c].style.backgroundColor = cellInfo.style.backgroundColor;
      }
    }

    let comments = [];
    let mergeCells = [];
    for (let r = 0; r < row; r++) {
      for (let c = 0; c < col; c++) {
        let cellMeta = myHandsOnTable.getCellMeta(r, c);
        //console.log(cellMeta);
        if (cellMeta.comment)
          comments.push({
            row: r,
            col: c,
            comment: { value: cellMeta.comment.value },
          });

        if (cellMeta.spanned) {
          let rowspan = myHandsOnTable.getCell(r, c).rowSpan;
          let colspan = myHandsOnTable.getCell(r, c).colSpan;

          mergeCells.push({ row: r, col: c, rowspan, colspan });
        }
      }
    }

    let json = { data, cellStyle, comments, mergeCells };
    // localStorage.setItem(ALL_DATA_KEY, JSON.stringify(json));

    let checkFileExist = await fileExist();
    if (checkFileExist) fileWrite(JSON.stringify(json, null, 2));
    else fileCreate(JSON.stringify(json, null, 2));
  };

  return (
    <Button
      sx={{ m: 2 }}
      variant="outlined"
      color="warning"
      onClick={saveGitHub}
    >
      깃허브 저장하기
    </Button>
  );
};

export default SaveButtonGitHub;

 

MyHandsonTable.js

import React, { useState, useEffect } from "react";
import CustomHansOnTable from "./CustomHansOnTable";

import Box from "@mui/material/Box";
import Drawer from "@mui/material/Drawer";
import Button from "@mui/material/Button";

import Divider from "@mui/material/Divider";

import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";

import FormControl from "@mui/material/FormControl";
import FormHelperText from "@mui/material/FormHelperText";
import Input from "@mui/material/Input";
import InputLabel from "@mui/material/InputLabel";

const MY_OPTIONS = "MY_OPTIONS";

const myDefaultOptions = {
  trueFalseOptions: {
    colHeaders: true,
    rowHeaders: true,
    wordWrap: false /* 줄 바꿈 off */,
    manualColumnResize: true,
    manualRowResize: true,
    manualColumnMove: true,
    manualRowMove: true,
    allowInsertColumn: true,
    allowInsertRow: true,
    allowRemoveColumn: true,
    allowRemoveRow: true,
    autoWrapCol: true /* 마지막 셀 아래에서 다음 셀 위로 이동 */,
    autoWrapRow: true /* 마지막 셀 옆에서 다음 셀 처음으로 이동 */,
    dragToScroll: true /* 표를 클릭 후 드래그를 할 때, 같이 스크롤 되는지 여부 */,
    persistentState: false /* 열 정렬 상태, 열 위치 및 열 크기를 로컬 스토리지에 저장 */,
    // outsideClickDeselects: false /* 셀 외부 클릭 시, 셀 선택 해제 */,
    readOnly: false /* true : 모든 셀을 readOnly로 설정*/,
    enterBeginsEditing: true /* true : 엔터 클릭 시 편집 모드, false : 다음 셀로 이동 */,
    copyable: true /* 복사 가능 여부 */,
    copyPaste: true /* 복사, 붙여넣기 가능 여부 */,
    undo: true /* false : ctrl + z 비활성화 */,
    trimWhitespace: false /* 자동 trim() 실행 후 셀에 저장 */,
    contextMenu: true /* 마우스 왼쪽 버튼 클릭 시 컨텍스트 메뉴 */,
    comments: true /* 주석, 메모 기능 context menu에 추가 */,
    manualColumnFreeze: true /* freezeColumn context menu에 추가 */,
    observeChanges: true,
  },

  numberOptions: {
    width: 1000,
    height: 1000,
    startCols: 5 /* data가 없는 경우 기본 설정 */,
    startRows: 5 /* data가 없는 경우 기본 설정 */,
    maxCols: 100 /* 주어진 값보다 큰 Column은 제거 */,
    maxRows: 100 /* 주어진 값보다 큰 Row는 제거 */,
    minCols: 1 /* 최소한의 Column */,
    minRows: 1 /* 최소한의 Row */,
    minSpareRows: 0 /* 빈 열 자동 추가 */,
    minSpareCols: 0 /* 빈 행 자동 추가 */,
    fixedColumnsLeft: 0,
    fixedRowsTop: 0,
    fixedRowsBottom: 0,
    rowHeaderWidth: 55 /* 행 헤더 너비 */,
  },

  cellInfo: {
    colWidths: 60,
    rowHeights: 25,
  },
};

const MyHandsonTable = () => {
  const [myOptions, setMyOptions] = useState(myDefaultOptions);

  const [state, setState] = useState({ right: false });

  const toggleDrawer = (anchor, open) => (event) => {
    if (
      event.type === "keydown" &&
      (event.key === "Tab" || event.key === "Shift")
    ) {
      return;
    }

    setState({ ...state, [anchor]: open });
  };

  const changeTrueFalseOptions = (option, value) => {
    console.log(myOptions);
    let temp = { ...myOptions };
    temp.trueFalseOptions[option] = !value;
    setMyOptions(temp);
    localStorage.setItem(MY_OPTIONS, JSON.stringify(temp));
  };

  const makeTrueFalseCheckBox = () => {
    let pair = Object.entries(myOptions.trueFalseOptions);

    pair = pair.map((item) => [item[0], Boolean(item[1])]);

    return pair.map((item, idx) => (
      <FormControlLabel
        key={idx}
        control={<Checkbox checked={item[1]} />}
        label={item[0]}
        onChange={() => changeTrueFalseOptions(item[0], item[1])}
      />
    ));
  };

  const changeNumberOptions = (option, value) => {
    let temp = { ...myOptions };

    if (isNaN(Number(value))) return;

    temp.numberOptions[option] = Number(value);
    setMyOptions(temp);
    localStorage.setItem(MY_OPTIONS, JSON.stringify(temp));
  };

  const makeNumberInput = () => {
    let pair = Object.entries(myOptions.numberOptions);

    pair = pair.map((item) => [item[0], Number(item[1])]);

    return pair.map((item, idx) => (
      <FormControl key={idx} sx={{ m: 2 }} variant="standard">
        <InputLabel htmlFor="component-error">{item[0]}</InputLabel>
        <Input
          value={item[1]}
          onChange={(e) => changeNumberOptions(item[0], e.target.value)}
        />
      </FormControl>
    ));
  };

  const list = () => (
    <Box sx={{ width: 600 }}>
      <Box sx={{ m: 2, flexDirection: "row" }}>{makeTrueFalseCheckBox()}</Box>

      <Divider />

      <Box sx={{ m: 2, flexDirection: "row" }}>
        <FormHelperText sx={{ color: "blue" }}>
          0 이상 숫자를 입력하세요.
        </FormHelperText>
        {makeNumberInput()}
      </Box>
    </Box>
  );

  const initLocalStorage = () => {
    let localOptions = localStorage.getItem(MY_OPTIONS);

    if (localOptions === null) return;

    setMyOptions(JSON.parse(localOptions));
  };

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

  return (
    <div>
      <div>
        {["right"].map((anchor) => (
          <React.Fragment key={anchor}>
            <Button
              sx={{ m: 2 }}
              variant="contained"
              color="secondary"
              onClick={toggleDrawer(anchor, true)}
            >
              Options Setting
            </Button>
            <Drawer
              anchor={anchor}
              open={state[anchor]}
              onClose={toggleDrawer(anchor, false)}
            >
              {list()}
            </Drawer>
          </React.Fragment>
        ))}
        <CustomHansOnTable myOptions={myOptions} />
      </div>
    </div>
  );
};

export default MyHandsonTable;

 

CustomHansOnTable.js

import React, { useEffect, useState } from "react";

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

import Handsontable from "handsontable";
import "handsontable/dist/handsontable.full.min.css";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";

import styled from "styled-components";
import HandsontableToggleButton from "./HandsontableToggleButton";
import SaveButtonGitHub from "./SaveButtonGitHub";

const DisplayCellStyle = styled.div`
  span {
    background-color: #33ceff;
    position: relative;
    padding: 0.4rem 0.85rem;
    border: 1px solid transparent;
    border-radius: 0.35rem;
  }
`;

// const data = [
//   ["", "Tesla", "Nissan", "Toyota", "Honda", "Mazda", "Ford"],
//   ["2017", 10, 11, 12, 13, 15, 16],
//   ["2018", 10, 11, 12, 13, 15, 16],
//   ["2019", 10, 11, 12, 13, 15, 16],
//   ["2020", 10, 11, 12, 13, 15, 16],
//   ["2021", 10, 11, 12, 13, 15, 16],
// ];

const data = [
  ["", "2017", "2018", "2019", "2020", "2021", "2022"],
  ["Tesla", 10, 5, 5, 10, 14, 5],
  ["Nissan", 15, 2, 7, 11, 13, 4],
  ["Toyota", 11, 1, 10, 12, 12, 3],
  ["Honda", 5, 3, 7, 13, 11, 4],
  ["Mazda", 4, 7, 5, 14, 10, 4],
];

// dummy data for test
const initData = () => {
  let row = [];
  for (let i = 0; i < 100; i++) {
    row.push(String.fromCharCode("A".charCodeAt() + (i % 26)));
  }

  let table = [];
  for (let k = 0; k < 100; k++) {
    let tmp = JSON.parse(JSON.stringify(row));
    let number = `${k + 1}`;
    for (let i = 0; i < 100; i++)
      tmp[i] = `${tmp[i]}${number.padStart(3, "0")}`;
    table.push(tmp);
  }

  return table;
};

let searchResultCount = 0;
function searchResultCounter(instance, row, col, value, result) {
  const DEFAULT_CALLBACK = function(instance, row, col, data, testResult) {
    instance.getCellMeta(row, col).isSearchResult = testResult;
  };

  DEFAULT_CALLBACK.apply(this, arguments);

  if (result) {
    searchResultCount++;
  }
}

function redRenderer(instance, td) {
  Handsontable.renderers.TextRenderer.apply(this, arguments);
  td.style.backgroundColor = "red";
  td.style.fontWeight = "bold";
}

const MY_OPTIONS = "MY_OPTIONS";
const ALL_DATA_KEY = "ALL_DATA_KEY";
let cellStyle = null;

const CustomHansOnTable = ({ myOptions }) => {
  const [myHandsOnTable, setMyHandsOnTable] = useState();
  const [displayCellInfo, setDisplaySetInfo] = useState("");
  const [selectedCell, setSelectedCell] = useState([0, 0]);

  const myNewQueryMethod = (searchValue, dataValue) => {
    if (!searchValue) return false;

    dataValue = dataValue || "";
    return searchValue.toString() === dataValue.toString();
  };

  const cellSelected = () => {
    let selectedLast = myTable.getSelectedLast();

    if (selectedLast[0] < 0 || selectedLast[1] < 0) return;

    let value = myTable.getValue() || "";
    setDisplaySetInfo(value);

    setSelectedCell([selectedLast[0], selectedLast[1]]);
  };

  const options = {
    data, // initData(),

    /* true or false options */
    colHeaders: true,
    rowHeaders: true,
    wordWrap: false /* 줄 바꿈 off */,
    manualColumnResize: true,
    manualRowResize: true,
    manualColumnMove: true,
    manualRowMove: true,
    allowInsertColumn: true,
    allowInsertRow: true,
    allowRemoveColumn: true,
    allowRemoveRow: true,
    autoWrapCol: true /* 마지막 셀 아래에서 다음 셀 위로 이동 */,
    autoWrapRow: true /* 마지막 셀 옆에서 다음 셀 처음으로 이동 */,
    dragToScroll: true /* 표를 클릭 후 드래그를 할 때, 같이 스크롤 되는지 여부 */,
    persistentState: false /* 열 정렬 상태, 열 위치 및 열 크기를 로컬 스토리지에 저장 */,
    outsideClickDeselects: false /* 셀 외부 클릭 시, 셀 선택 해제 */,
    readOnly: false /* true : 모든 셀을 readOnly로 설정*/,
    enterBeginsEditing: true /* true : 엔터 클릭 시 편집 모드, false : 다음 셀로 이동 */,
    copyable: true /* 복사 가능 여부 */,
    copyPaste: true /* 복사, 붙여넣기 가능 여부 */,
    undo: true /* false : ctrl + z 비활성화 */,
    trimWhitespace: false /* 자동 trim() 실행 후 셀에 저장 */,
    contextMenu: true /* 마우스 왼쪽 버튼 클릭 시 컨텍스트 메뉴 */,
    comments: true /* 주석, 메모 기능 context menu에 추가 */,
    manualColumnFreeze: true /* freezeColumn context menu에 추가 */,

    observeChanges: true,
    afterChangesObserved: () => {
      //console.log("change !!");
    },

    // filters: true, /* 필터 기능 on 6.2.2 pro  */,
    // dropdownMenu: true, /* dropdown 메뉴 설정 6.2.2 pro */

    /* Selected Options */
    className: "htMiddle htCenter" /* Cell Alignment */,
    // stretchH: "none", /* 빈 공간을 채우는 방법 : none, last, all */
    // selectionMode: "multiple", /* Ctrl 키 + 선택 가능한 셀 : multiple, range, single */
    // fillHandle : true, /* 드래그로 자동 채움 : true, false, vertical, horizontal 옵션 */
    // disableVisualSelection: "current", /* 셀 선택 활성화 여부 : false, true, current, area, header, [option1, option2, ...] */

    /* Number Options */
    width: 1000,
    height: 1000,

    startCols: 5 /* data가 없는 경우 기본 설정 */,
    startRows: 3 /* data가 없는 경우 기본 설정 */,
    afterSelection: cellSelected,
    // maxCols: 2, /* 주어진 값보다 큰 Column은 제거 */
    // maxRows: 3, /* 주어진 값보다 큰 Row는 제거 */
    // minCols: 10, /* 최소한의 Column */
    // minRows: 10, /* 최소한의 Row */
    // minSpareRows: 1, /* 빈 열 자동 추가 */
    // minSpareCols: 2, /* 빈 행 자동 추가 */
    // fixedColumnsLeft: 2,
    // fixedRowsTop: 3,
    // fixedRowsBottom: 2,
    // rowHeaderWidth: 250, /* 행 헤더 너비 */

    /* Customizing Options */
    colWidths: 60 /* 특정 위치 너비 변경 : [60, 120, 60, 60, 60, 60, 60] */,
    rowHeights: 25,
    // placeholder: 'Empty',
    // columnSorting: {
    //   indicator: true, /* default true, 정렬 순서 표시 마크 (↑↓) on / off */
    //   sortEmptyCells: true, /* true : 빈 셀도 정렬, false : 모든 빈 셀은 테이블 끝으로 이동 */
    //   headerAction: true, /* default true, 헤더 클릭 시 정렬 기능 on / off */
    //   initialConfig: {
    //     column: 2, /* column : 2를 기준으로 정렬 */
    //     sortOrder: "asc", /* 내림차순 desc */
    //   },

    //   /* 비교함수 구현. -1, 0, 1을 return. */
    //   // compareFunctionFactory: function(sortOrder, columnMeta) {
    //   //   return function(value, nextValue) {
    //   //     if(value > 2000) return -1;
    //   //     return value - nextValue;
    //   //   }
    //   // },
    // },

    comments: {
      displayDelay: 1000 /* 1초 뒤에 메모가 on */,
    },

    cell: [],

    // 6.2.2 미지원
    // beforeSetCellMeta:(row, col, key, value) => {
    //   console.log("before",row, col, key, value);
    // },

    // afterChange: function(change, source) {
    //   console.log(change, source);
    //   //change [row, col, before, after];
    // },

    mergeCells: [],

    // search: {
    //   callback: searchResultCounter,
    //   queryMethod: myNewQueryMethod,
    //   //searchResultClass: 'customClass'
    // },

    // columns: [
    //     {data: "id", type: 'numeric'},
    //     {data: "name", renderer: redRenderer},
    //     {data: "isActive", type: 'checkbox'},
    //     {data: "date", type: 'date', dateFormat: 'YYYY-MM-DD'},
    //     {data: "color",
    //       type: 'autocomplete', // dropdown
    //       source: ["yellow", "red", "orange", "green", "blue", "gray", "black", "white"]
    //     },
    //     {
    //       editor: 'select',
    //       selectOptions: ['Kia', 'Nissan', 'Toyota', 'Honda']
    //     },
    //   ],

    cells: function(row, col, prop) {
      if (
        cellStyle === null ||
        cellStyle[row] === undefined || // insert 방어 코드
        cellStyle[row][col] === undefined
      )
        return {};

      let cellProperties = {};

      cellProperties.className = cellStyle[row][col].className || "htCenter htMiddle"; // undefined 처리

      cellProperties.renderer = function(instance, td) {
        Handsontable.renderers.TextRenderer.apply(this, arguments);
        td.style.fontWeight = cellStyle[row][col].style.fontWeight || "";
        td.style.fontStyle = cellStyle[row][col].style.fontStyle || "";
        td.style.textDecoration = cellStyle[row][col].style.textDecoration || "";
        td.style.color = cellStyle[row][col].style.color || "#000000";
        td.style.backgroundColor = cellStyle[row][col].style.backgroundColor || "#FFFFFF";
      };

      return cellProperties;
    },

    licenseKey: "non-commercial-and-evaluation",
  };

  const setColWidths = (table, setOptions) => {
    let colLength = table.getData()[0].length;
    let widths = [];

    for (let i = 0; i < colLength; i++) widths.push(table.getColWidth(i));

    setOptions.cellInfo.colWidths = widths;

    localStorage.setItem(MY_OPTIONS, JSON.stringify(setOptions));
  };

  const setRowHeights = (table, setOptions) => {
    let rowLength = table.getData().length;
    let heights = [];

    for (let i = 0; i < rowLength; i++) heights.push(table.getRowHeight(i));

    setOptions.cellInfo.rowHeights = heights;

    localStorage.setItem(MY_OPTIONS, JSON.stringify(setOptions));
  };

  let myKey = process.env.REACT_APP_MY_TOKEN;
  const fileRead = async () => {
    const repo = `auto-test`; // my repository
    const path = `handsontable.json`; // fileName;

    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) {
      return undefined;
    }
  };

  let myTable;
  const makeTable = async () => {
    // let allData = localStorage.getItem(ALL_DATA_KEY);
    // if (allData !== null) {
    //   allData = JSON.parse(allData);
    //   options.data = allData.data;
    //   options.cell = allData.comments;
    //   options.mergeCells = allData.mergeCells;
    //   cellStyle = allData.cellStyle;
    // }

    let allData = await fileRead();
    if (allData !== undefined) {
      allData = JSON.parse(allData);
      options.data = allData.data;
      options.cell = allData.comments;
      options.mergeCells = allData.mergeCells;
      cellStyle = allData.cellStyle;
    }

    const container = document.getElementById("hot-app");
    container.innerHTML = "";

    myTable = new Handsontable(container, {
      ...options,
      ...myOptions.trueFalseOptions,
      ...myOptions.numberOptions,
      ...myOptions.cellInfo,
    });

    myTable.addHook("afterColumnResize", function(col, width) {
      let localOptions = localStorage.getItem(MY_OPTIONS);

      if (localOptions === null) {
        setColWidths(this, myOptions);
        return;
      }

      localOptions = JSON.parse(localOptions);
      if (Array.isArray(localOptions.cellInfo.colWidths) === false) {
        setColWidths(this, localOptions);
        return;
      }

      localOptions.cellInfo.colWidths[col] = width;
      localStorage.setItem(MY_OPTIONS, JSON.stringify(localOptions));
    });

    myTable.addHook("afterRowResize", function(row, height) {
      let localOptions = localStorage.getItem(MY_OPTIONS);

      if (localOptions === null) {
        setRowHeights(this, myOptions);
        return;
      }

      localOptions = JSON.parse(localOptions);
      if (Array.isArray(localOptions.cellInfo.rowHeights) === false) {
        setRowHeights(this, localOptions);
        return;
      }

      localOptions.cellInfo.rowHeights[row] = height;
      localStorage.setItem(MY_OPTIONS, JSON.stringify(localOptions));
    });

    myTable.render();
    setMyHandsOnTable(myTable);

    // search 구현
    // let searchField = document.getElementById("search_field");
    // let resultCount = document.getElementById("resultCount");

    // Handsontable.dom.addEvent(searchField, "keyup", function(event) {
    //   searchResultCount = 0;

    //   let search = myTable.getPlugin("search");
    //   let queryResult = search.query(this.value);

    //   console.log(queryResult);

    //   resultCount.innerText = searchResultCount.toString();
    //   myTable.render();
    // });
  };

  useEffect(() => {
    makeTable();
  }, [myOptions]);

  const changeFormat = (value) => {
    value = value || "";
    value = value.toString();
    if (value.includes('"')) return '"' + value.replace(/"/g, '""') + '"';
    if (value.includes(",") || value.includes("\n")) return '"' + value + '"';
    return value;
  };

  const downloadCSV = () => {
    let data = myHandsOnTable.getData();

    let csv = "";
    for (let r = 0; r < data.length; r++) {
      let row = data[r].map(changeFormat).join(",");
      csv += row + "\n";
    }

    let fileDown = "data:csv;charset=utf-8," + csv;
    let encodedUri = encodeURI(fileDown);
    let link = document.createElement("a");

    link.setAttribute("href", encodedUri);
    link.setAttribute("download", "handsontable.csv");

    document.body.appendChild(link);

    link.click();

    document.body.removeChild(link);
  };

  return (
    <div>
      <Box sx={{ m: 2 }}>
        <Button
          sx={{ m: 2 }}
          variant="outlined"
          color="primary"
          onClick={downloadCSV}
        >
          Download CSV
        </Button>
        <SaveButtonGitHub myHandsOnTable={myHandsOnTable} />
        {/* <input id="search_field" type="search" placeholder="search" />
        <p>
        <span id="resultCount">0</span> results
      </p> */}
        <HandsontableToggleButton
          myHandsOnTable={myHandsOnTable}
          selectedCell={selectedCell}
        />
        <DisplayCellStyle>
          <span>{displayCellInfo}</span>
        </DisplayCellStyle>
        <div id="hot-app" style={{ marginTop: "13px" }}></div>
      </Box>
    </div>
  );
};

export default CustomHansOnTable;

 

HandsontableToggleButton.js

import React, { useState } from "react";

import { styled } from "@mui/material/styles";

import Box from "@mui/material/Box";
import FormatAlignLeftIcon from "@mui/icons-material/FormatAlignLeft";
import FormatAlignCenterIcon from "@mui/icons-material/FormatAlignCenter";
import FormatAlignRightIcon from "@mui/icons-material/FormatAlignRight";
//import FormatAlignJustifyIcon from "@mui/icons-material/FormatAlignJustify";
import VerticalAlignBottomIcon from "@mui/icons-material/VerticalAlignBottom";
import VerticalAlignCenterIcon from "@mui/icons-material/VerticalAlignCenter";
import VerticalAlignTopIcon from "@mui/icons-material/VerticalAlignTop";
import FormatBoldIcon from "@mui/icons-material/FormatBold";
import FormatItalicIcon from "@mui/icons-material/FormatItalic";
import FormatUnderlinedIcon from "@mui/icons-material/FormatUnderlined";
import FormatStrikethroughIcon from "@mui/icons-material/FormatStrikethrough";
import FormatColorFillIcon from "@mui/icons-material/FormatColorFill";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import Divider from "@mui/material/Divider";
import Paper from "@mui/material/Paper";
import ColorizeIcon from "@mui/icons-material/Colorize";
import ToggleButton from "@mui/material/ToggleButton";
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";

import { CompactPicker } from "react-color";
import { useEffect } from "react";

const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
  "& .MuiToggleButtonGroup-grouped": {
    margin: theme.spacing(0.5),
    border: 0,
    "&.Mui-disabled": {
      border: 0,
    },
    "&:not(:first-of-type)": {
      borderRadius: theme.shape.borderRadius,
    },
    "&:first-of-type": {
      borderRadius: theme.shape.borderRadius,
    },
  },
}));

const HandsontableToggleButton = ({ myHandsOnTable, selectedCell }) => {
  const [horizontalAlignment, setHorizontalAlignment] = useState("");
  const [verticalAlignment, setVerticalAlignment] = useState("");
  const [formats, setFormats] = useState(() => []);

  const [showCompactPicker, setShowCompactPicker] = useState(false);
  const [pickerPosition, setPickerPosition] = useState({ top: 0, left: 0 });
  const [fontColor, setFontColor] = useState("#000000");
  const [bgColor, setBgColor] = useState("#FFFFFF");

  const getCellInfoBase = () => {
    let selecetedRangeCells = myHandsOnTable.getSelectedRange();
    if (selecetedRangeCells === undefined) return undefined;

    let baseCell = selecetedRangeCells[0].from;
    return myHandsOnTable.getCell(baseCell.row, baseCell.col);
  };

  const getCellInfoRange = () => {
    let selecetedRangeCells = myHandsOnTable.getSelectedRange();
    if (selecetedRangeCells === undefined) return undefined;

    let cellPositions = [];
    for (let cell of selecetedRangeCells) {
      for (let r = cell.from.row; r <= cell.to.row; r++) {
        for (let c = cell.from.col; c <= cell.to.col; c++)
          cellPositions.push([r, c]);
      }
    }

    return cellPositions;
  };

  const getHorizontalStatus = (className) => {
    let status = ["htLeft", "htCenter", "htRight"];
    let current = className.split(" ");

    return current.filter((item) => status.includes(item))[0];
  };

  const getVerticalStatus = (className) => {
    let status = ["htTop", "htMiddle", "htBottom"];
    let current = className.split(" ");

    return current.filter((item) => status.includes(item))[0];
  };

  const handleAlignment = (event, newAlignment, type) => {
    console.log(newAlignment, type);

    let cellPositions = getCellInfoRange();
    if (cellPositions === undefined) return;

    if (type === "horizontal") setHorizontalAlignment(newAlignment);
    else if (type === "vertical") setVerticalAlignment(newAlignment);

    for (let pos of cellPositions) {
      let cellInfo = myHandsOnTable.getCell(pos[0], pos[1]);
      let className = cellInfo.className;
      let split = className.split(" ");
      if (type === "horizontal") {
        let horizontal = getHorizontalStatus(className);
        split = split.filter((item) => item !== horizontal); // 현재 설정 값 삭제
      } else if (type === "vertical") {
        let vertical = getVerticalStatus(className);
        split = split.filter((item) => item !== vertical); // 현재 설정 값 삭제
      }

      if (newAlignment) split.push(newAlignment); // 새로 설정된 값 추가.

      cellInfo.className = split.join(" ");
    }
  };

  const handleFormat = (event, newFormats) => {
    console.log(newFormats);

    let cellPositions = getCellInfoRange();
    if (cellPositions === undefined) return;

    setFormats(newFormats);

    for (let pos of cellPositions) {
      let cellInfo = myHandsOnTable.getCell(pos[0], pos[1]);

      cellInfo.style.fontWeight = newFormats.includes("bold") ? "bold" : "";
      cellInfo.style.fontStyle = newFormats.includes("italic") ? "italic" : "";

      let deco = [];
      if (newFormats.includes("underline")) deco.push("underline");
      if (newFormats.includes("line-through")) deco.push("line-through");

      cellInfo.style.textDecoration = deco.join(" ");
    }
  };

  const handleToggleCompactPicker = (event, type) => {
    let cellPositions = getCellInfoRange();
    if (cellPositions === undefined) return;

    const iconButton = event.currentTarget;
    const rect = iconButton.getBoundingClientRect();
    const pickerTop = rect.bottom + window.scrollY;
    const pickerLeft = rect.left + window.scrollX;

    setPickerPosition({ top: pickerTop, left: pickerLeft });
    setShowCompactPicker((prev) => !prev);
  };

  const handleChangeComplete = (color, event) => {
    let cellPositions = getCellInfoRange();
    if (cellPositions === undefined) return;

    let colorType = formats.includes("fontColor") ? "fontColor" : "bgColor";

    console.log(colorType, color.hex);

    if (colorType === "fontColor") setFontColor(color.hex);
    else setBgColor(color.hex);

    for (let pos of cellPositions) {
      let cellInfo = myHandsOnTable.getCell(pos[0], pos[1]);

      if (colorType === "fontColor") {
        cellInfo.style.color = color.hex;
      } else {
        cellInfo.style.backgroundColor = color.hex;
      }
    }
  };

  const getColorPicker = () => {
    let colorType = formats.includes("fontColor") ? "fontColor" : "bgColor";
    return (
      <CompactPicker
        color={colorType === "fontColor" ? fontColor : bgColor}
        onChangeComplete={handleChangeComplete}
      />
    );
  };

  const handleClose = () => {
    let fms = formats.filter(
      (item) => (item === "fontColor" || item === "bgColor") === false
    );
    setFormats(fms);

    setShowCompactPicker(false);
  };

  const setButtonState = () => {
    if (myHandsOnTable === undefined) return;

    let cellInfo = getCellInfoBase();
    let className = cellInfo.className;
    let horizontal = getHorizontalStatus(className) || ""; // undefined 처리
    let vertical = getVerticalStatus(className) || "";

    setHorizontalAlignment(horizontal);
    setVerticalAlignment(vertical);

    let fontWeight = cellInfo.style.fontWeight;
    let fontStyle = cellInfo.style.fontStyle;
    let textDecoration = cellInfo.style.textDecoration.split(" ");

    setFormats([fontWeight, fontStyle, ...textDecoration]);
    setFontColor(cellInfo.style.color);
    setBgColor(cellInfo.style.backgroundColor);
  };

  useEffect(() => {
    setButtonState();
  }, [selectedCell]);

  return (
    <div>
      <Box sx={{ m: 2, marginBottom: 5 }}>
        <Paper
          elevation={0}
          sx={{
            display: "flex",
            border: (theme) => `1px solid ${theme.palette.divider}`,
            flexWrap: "wrap",
            width: "580px",
          }}
        >
          <StyledToggleButtonGroup
            size="small"
            value={horizontalAlignment}
            exclusive
            onChange={(e, alignment) =>
              handleAlignment(e, alignment, "horizontal")
            }
            aria-label="text alignment"
          >
            <ToggleButton value="htLeft" aria-label="left aligned">
              <FormatAlignLeftIcon />
            </ToggleButton>
            <ToggleButton value="htCenter" aria-label="centered">
              <FormatAlignCenterIcon />
            </ToggleButton>
            <ToggleButton value="htRight" aria-label="right aligned">
              <FormatAlignRightIcon />
            </ToggleButton>
            {/* <ToggleButton value="justify" aria-label="justified">
              <FormatAlignJustifyIcon />
            </ToggleButton> */}
          </StyledToggleButtonGroup>

          <StyledToggleButtonGroup
            size="small"
            value={verticalAlignment}
            exclusive
            onChange={(e, alignment) =>
              handleAlignment(e, alignment, "vertical")
            }
            aria-label="text alignment"
          >
            <ToggleButton value="htTop" aria-label="top aligned">
              <VerticalAlignTopIcon />
            </ToggleButton>
            <ToggleButton value="htMiddle" aria-label="middle">
              <VerticalAlignCenterIcon />
            </ToggleButton>
            <ToggleButton value="htBottom" aria-label="bottom aligned">
              <VerticalAlignBottomIcon />
            </ToggleButton>
          </StyledToggleButtonGroup>

          <Divider flexItem orientation="vertical" sx={{ mx: 0.5, my: 1 }} />

          <StyledToggleButtonGroup
            size="small"
            value={formats}
            onChange={handleFormat}
            aria-label="text formatting"
          >
            <ToggleButton value="bold" aria-label="bold">
              <FormatBoldIcon />
            </ToggleButton>
            <ToggleButton value="italic" aria-label="italic">
              <FormatItalicIcon />
            </ToggleButton>
            <ToggleButton value="underline" aria-label="underline">
              <FormatUnderlinedIcon />
            </ToggleButton>
            <ToggleButton value="line-through" aria-label="line-through">
              <FormatStrikethroughIcon />
            </ToggleButton>

            <ToggleButton
              value="fontColor"
              aria-label="fontColor"
              onClick={(e) => handleToggleCompactPicker(e, "fontColor")}
            >
              <ColorizeIcon />
              <ArrowDropDownIcon />
            </ToggleButton>
            <ToggleButton
              value="bgColor"
              aria-label="bgColor"
              onClick={(e) => handleToggleCompactPicker(e, "bgColor")}
            >
              <FormatColorFillIcon />
              <ArrowDropDownIcon />
            </ToggleButton>
          </StyledToggleButtonGroup>
        </Paper>

        {showCompactPicker && (
          <div
            className="compact-picker-container"
            style={{
              position: "absolute",
              top: pickerPosition.top + "px",
              left: pickerPosition.left + "px",
              zIndex: 1000,
            }}
          >
            <div
              style={{
                position: "fixed",
                top: "0px",
                right: "0px",
                bottom: "0px",
                left: "0px",
              }}
              onClick={handleClose}
            />
            {getColorPicker()}
          </div>
        )}
      </Box>
    </div>
  );
};

export default HandsontableToggleButton;
반응형

댓글