본문 바로가기
개발/React

리액트 - Mui Drawer로 Handsontable Option 관리하기

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

리액트 전체 링크

 

참고

- https://mui.com/material-ui/react-drawer/

- 오브젝트 순회하기 (Iterate JavaScript Object)

- 로컬 스토리지 사용 방법과 세션 스토리지 비교

 

- 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 저장하기)


Material UI의 Drawer 코드를 참고해서 아래와 같은 템플릿을 만들자.

 

예시 코드는 다음과 같다.

import React, { useState } 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 FormGroup from "@mui/material/FormGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";

const MyHandsonTable = () => {
  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 list = () => (
    <Box sx={{ width: 1000 }}>
      <FormGroup>
        <FormControlLabel control={<Checkbox checked={true} />} label="Label" />
        <FormControlLabel required control={<Checkbox />} label="Required" />
        <FormControlLabel disabled control={<Checkbox />} label="Disabled" />
      </FormGroup>
      <Divider />
      <FormGroup>
        <FormControlLabel control={<Checkbox checked={true} />} label="Label" />
        <FormControlLabel required control={<Checkbox />} label="Required" />
        <FormControlLabel disabled control={<Checkbox />} label="Disabled" />
      </FormGroup>
    </Box>
  );

  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(anchor)}
            </Drawer>
          </React.Fragment>
        ))}
        {/* <CustomHansOnTable /> */}
      </div>
    </div>
  );
};

export default MyHandsonTable;

 


True False Options 구현

 

커스터마이징 하고 싶은 True / False 옵션을 아래와 같이 기본값으로 설정한다.

  const [myOptions, setMyOptions] = useState({
    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: true /* 셀 외부 클릭 시, 셀 선택 해제 */,
      readOnly: true /* 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: { ... },
  });

 

True / False 옵션이기 때문에 Mui Check Box를 만든다.

myOptions.trueFalseOptions를 배열로 만들고 map을 이용하였다.

그리고 onChange에 해당 값만 변경할 수 있도록 이벤트를 추가하였다.

  const changeTrueFalseOptions = (option, value) => {
    let temp = { ...myOptions };
    temp.trueFalseOptions[option] = !value;
    setMyOptions(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])}
      />
    ));
  };

 

Box 안에 makeTrueFalseCheckBox를 추가한다.

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

 

이제 Drawer에 아래와 같이 옵션이 나타나게 된다.

 

Handsontable에는 현재 옵션을 props로 넘겨준다.

<CustomHansOnTable myOptions={myOptions} />

 

그리고 myTable을 만들 때, 해당 optionsmyOptions덮어씌운다.

const CustomHansOnTable = ({ myOptions }) => {

  ...
  
  let myTable;
  const makeTable = () => {
    const container = document.getElementById("hot-app");
    container.innerHTML = "";

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

 

이제 True / False 옵션을 원하는 대로 커스터마이징 할 수 있다.


Number Options 구현

 

위와 마찬가지로 커스터마이징 하고 싶은 Number Options을 추가한다.

  const [myOptions, setMyOptions] = useState({
    trueFalseOptions: { ... },

    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 /* 행 헤더 너비 */,
    },
  });

 

Number는 값을 입력하기 때문에 Mui Input을 사용하였다.

그리고 숫자 외의 값은 입력되지 않도록 처리한다.

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

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

    temp.numberOptions[option] = Number(value);
    setMyOptions(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>
    ));
  };

 

list에 makeNumberInput을 추가한다.

  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>
  );

 

테이블을 생성하는 위치에 numberOptions덮어씌우도록 한다.

  let myTable;
  const makeTable = () => {
    const container = document.getElementById("hot-app");
    container.innerHTML = "";

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

 

원하는 대로 커스터마이징 되는지 확인해 보자.

 

전체 코드는 다음과 같다.

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 MyHandsonTable = () => {
  const [myOptions, setMyOptions] = useState({
    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: true /* 셀 외부 클릭 시, 셀 선택 해제 */,
      readOnly: true /* 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 /* 행 헤더 너비 */,
    },
  });

  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) => {
    let temp = { ...myOptions };
    temp.trueFalseOptions[option] = !value;
    setMyOptions(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);
  };

  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>
  );
  
  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;

로컬 스토리지에 옵션 저장하기

 

마지막으로 커스터마이징한 옵션이 로컬에 남도록 해보자.

 

로컬 스토리지의 keyMY_OPTIONS를 정의한다.

그리고 useState에 있던 기본값을 myDefaultOptions로 정의한다.

const MY_OPTIONS = "MY_OPTIONS";

const myDefaultOptions = {
  trueFalseOptions: { ... },
  numberOptions: { ... },
}

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

 

로컬 스토리지의 초기화는 useEffect를 이용한다.

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

    if (localOptions === null) return;

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

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

 

그리고 setMyOptions설정한 옵션localStorage에도 저장하도록 코드를 추가한다.

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

  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)); // 추가
  };

 

이제 새로고침을 해도 설정이 저장되는 것을 확인할 수 있다.

 

전체 코드는 다음과 같다.

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: true /* 셀 외부 클릭 시, 셀 선택 해제 */,
    readOnly: true /* 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 /* 행 헤더 너비 */,
  },
};

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

댓글