본문 바로가기
개발/React

리액트, Node JS - 행 / 열 이동 및 크기 변경 연동하기 (Sync Row / Column Movement and Size Adjustment)

by 피로물든딸기 2024. 4. 13.
반응형

리액트 전체 링크

Node JS 전체 링크

 

참고

- https://handsontable.com/docs/javascript-data-grid/api/hooks/#afterrowmove

- 전치 행렬 구하기

 

스프레드시트 공동 작업하기 Project Settings
Handsontable에 Socket.IO 적용하기
변경된 셀만 데이터 전송해서 최적화하기
행 / 열 이동 및 크기 변경 연동하기
다른 클라이언트가 선택한 셀 표시하기

 

이제 행이나 열이동하거나, 삭제, 추가 그리고 크기 변경도 연동해 보자.


행 / 열 이동, 삭제, 추가 구현

 

afterRow / ColumnMove에는 여러 parameter가 있지만,

여기서는 movedRows / ColumnsfinalIndex만 사용하는 경우만 처리한다.

 

먼저, movedRowsfinalIndex실제 2D 테이블을 변경하는 메서드를 만들자.

movedRows에 해당하는 배열을 복사해서 옮긴 후, 이전의 배열을 삭제하는 식으로 구현하였다.

const moveRowTable = (table, movedRows, finalIndex) => {
  let keyTable = table.map((item, key) => {
    return {
      index : key,
      row : item,
    }
  });

  let movedTable = table.filter((item, key) => movedRows.includes(key) === true);
  let movedKeyTable = movedTable.map((item) => {
    return {
      index : "new",
      row : item,
    }
  });
  
  let left = keyTable.slice(0, finalIndex);
  let right = keyTable.slice(finalIndex);
  let newArrayKey = [...left, ...movedKeyTable, ...right];

  newArrayKey = newArrayKey.filter((item) => movedRows.includes(item.index) === false);
  let newArray = newArrayKey.map(item => item.row);

  return newArray;
};

 

Column 이동은 따로 구현할 필요 없이 전치 행렬 구하기transpose 메서드를 참고하면 된다.

table의 행 / 열전환하고, moveRowTable을 한 후, 다시 행 / 열을 전환하였다.

const moveColumnTable = (table, movedColumns, finalIndex) => {
  let transposedTable = transpose(table);
  let movedTable = moveRowTable(transposedTable, movedColumns, finalIndex);
  return transpose(movedTable);
}

 

이제 위의 메서드를 서버 클라이언트에 모두 추가한다.

 

서버는 현재 datamoveRowTablemoveColumnTable을 이용해서 갱신한다.

그리고 movedRows / ColumnsfinalIndex클라이언트로 전송한다.

  socket.on("moveRow", (movedRows, finalIndex) => {    
    console.log("moveRow", movedRows, finalIndex);
    currentData = moveRowTable(currentData, movedRows, finalIndex);
    socket.broadcast.emit("resMoveRow", movedRows, finalIndex);
  });  

  socket.on("moveCol", (movedColumns, finalIndex) => {    
    console.log("moveCol", movedColumns, finalIndex);
    currentData = moveColumnTable(currentData, movedColumns, finalIndex);
    socket.broadcast.emit("resMoveCol", movedColumns, finalIndex);
  });

 

클라이언트movedRowsfinalIndex를 서버로 전송하면 된다.

그리고 setTableData현재 data를 갱신해야 한다.

setTableData를 사용하기 위해 customOptionsSocketTable 내부로 옮겼다.

    afterRowMove: function (movedRows, finalIndex, dropIndex, movePossible, orderChanged) {
      console.log("move row", movedRows, finalIndex, dropIndex, movePossible, orderChanged);

      setTableData(this.getData());  
      socketIO.emit("moveRow", movedRows, finalIndex);
    },

 

서버와 마찬가지로 콜백을 받은 클라이언트는 자신의 데이터를 갱신하면 된다.

  const resMoveRow = (movedRows, finalIndex) => {
    console.log("resMoveRow", movedRows, finalIndex);
    setTableData((prev) => {
      let newTable = moveRowTable(prev, movedRows, finalIndex);
      return newTable;
    });
  };

 

삭제추가주어지는 정보를 함께 연동하면 된다.

나머지는 전체 코드를 참고하자.


행 / 열 크기 변경 구현

 

주어지는 hook에서는 몇 번째 row / col이 변경되는지 알려주고 있다.

 

그런데 크기를 변경하는 것은 생각보다 까다롭다.

데이터를 편집하거나 행 / 열을 변경할 때, size를 같이 넘겨주지 않으면,

공동으로 편집하는 클라이언트의 size가 초기화된다.

 

따라서 size전체 값을 통째로 연동한다.

먼저 서버는 현재 rowcolsize를 가지고 있어야하고, setRowHeightssetColWidths로 초기화한다.

let currentRowHeights = undefined;

  socket.on("setRowHeights", (initRowHeights) => {    
    console.log("setRowHeights", initRowHeights);
    currentRowHeights = initRowHeights;
  });

 

size 변경 message는 다음과 같다.

  socket.on("resizeRow", (rowHeights) => {    
    console.log("resizeRow", rowHeights);
    currentRowHeights = rowHeights;
    socket.broadcast.emit("resResizeRow", rowHeights);
  });

 

그리고 이전에 만든 메시지에도 현재 row / col 값을 전달하도록 코드를 수정한다. (전체 코드 참고)

  socket.on("createRow", (index, amount, rowHeights) => {    
    console.log("createRow", index, amount);
    currentData = insertRows(currentData, index, amount);
    currentRowHeights = rowHeights
    socket.broadcast.emit("resCreateRow", index, amount, currentRowHeights);
  });
  
  ... 그 외 메서드

 

클라이언트table row / col size useState로 관리한다.

  const [rowHeights, setRowHeights] = useState(25);
  const [colWidths, setColWidths] = useState(60);
  
  ...
  
  <SimpleHandsOnTable
    data={tableData}
    rowHeights={rowHeights}
    colWidths={colWidths}
    customOptions={customOptions}
  />

 

SimpleHandsOnTable의 옵션의 기본값으로 초기화하였다.

  const options = {
    ...
    
    colWidths: 60,
    rowHeights: 25,

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

 

SimpleHandsOnTablepropsrowHeightscolWidths를 추가한다.

const SimpleHandsOnTable = ({ data, rowHeights, colWidths, setTable, customOptions }) => {

 

options에는 기본값을 지우고 props로 대체한다.

  const options = {
    ...
    
    colWidths,
    rowHeights,

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

 

이제 size 변경이 일어나면 다시 테이블을 갱신한다.

  useEffect(() => {
    makeTable();
  }, [data, rowHeights, colWidths]);

 

SocketTable테이블의 사이즈를 초기화해서 서버로 보내고, 갱신하는 코드를 추가한다.

  useEffect(() => {
    socketIO.once("initData", (data) => {
      console.log(data);
      if (data.currentData) {
        setTableData(data.currentData);
        setRowHeights(data.currentRowHeights);
        setColWidths(data.currentColWidths);
      } else {
        let basicRowHeight = 25;
        let basicColWidth = 60;
        let initRowHeights = new Array(defaultData.length).fill(basicRowHeight);
        let initColWidths = new Array(defaultData[0].length).fill(
          basicColWidth
        );

        socketIO.emit("setCurrentData", tableData);
        socketIO.emit("setRowHeights", initRowHeights);
        socketIO.emit("setColWidths", initColWidths);
      }
    });
  }, []);

 

사이즈 변경에 대해서는 useState로 갱신하면 된다.

  const resResizeRow = (rowHeights) => {
    console.log("resResizeRow", rowHeights);
    setRowHeights(rowHeights);
  };

  const resResizeCol = (colWidths) => {
    console.log("resResizeCol", colWidths);
    setColWidths(colWidths);
  };

 

그 외 관련 메서드에도 size를 갱신하는 코드를 모두 추가하자. (전체 코드 참고)

  afterRowMove: function (movedRows, finalIndex, dropIndex, movePossible, orderChanged) {
    console.log("move row", movedRows, finalIndex, dropIndex, movePossible, orderChanged);

    let rowHeights = getRowHeights(this);

    setRowHeights(rowHeights);
    setTableData(this.getData());  
    socketIO.emit("moveRow", movedRows, finalIndex, rowHeights);
  },
  
  ...

  const resMoveRow = (movedRows, finalIndex, currentRowHeights) => {
    console.log("resMoveRow", movedRows, finalIndex, currentRowHeights);
    setTableData((prev) => {
      let newTable = moveRowTable(prev, movedRows, finalIndex);
      return newTable;
    });
    
    setRowHeights(currentRowHeights);
  };

 

여기까지 SocketTable.js는 다음과 같다.

import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";

import styles from "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";

import { io } from "socket.io-client";

import {
  Avatar,
  Conversation,
  MessageSeparator,
} from "@chatscope/chat-ui-kit-react";

import SimpleHandsOnTable from "./SimpleHandsOnTable";

const AVATAR_MAP = {
  Lilly: "https://chatscope.io/storybook/react/assets/lilly-aj6lnGPk.svg",
  Joe: "https://chatscope.io/storybook/react/assets/joe-v8Vy3KOS.svg",
  Emily: "https://chatscope.io/storybook/react/assets/emily-xzL8sDL2.svg",
  Akane: "https://chatscope.io/storybook/react/assets/akane-MXhWvx63.svg",
  Eliot: "https://chatscope.io/storybook/react/assets/eliot-JNkqSAth.svg",
  Zoe: "https://chatscope.io/storybook/react/assets/zoe-E7ZdmXF0.svg",
};

const defaultData = [
  ["", "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],
];

let socketIO = io("http://localhost:3333", { autoConnect: false });

const getRowHeights = (handsOnTable) => {
  let countRows = handsOnTable.countRows();
  let rowHeights = [];
  for(let r = 0; r < countRows; r++) rowHeights.push(handsOnTable.getRowHeight(r));
  return rowHeights;
}

const getColWidths = (handsOnTable) => {
  let countCols = handsOnTable.countCols();
  let colWidths = [];
  for(let c = 0; c < countCols; c++) colWidths.push(handsOnTable.getColWidth(c));
  return colWidths;
}

const insertRows = (table, rowIndex, amount) => {
  const afterIndex = table.slice(rowIndex);
  const emptyArray = Array.from({ length: table[0].length }, () => "");
  const newRows = Array.from({ length: amount }, () => emptyArray);
  const newArray = table.slice(0, rowIndex).concat(newRows, afterIndex);

  return newArray;
};

const deleteRows = (table, rowIndex, amount) => {
  const newTable = [...table];

  newTable.splice(rowIndex, amount);

  return newTable;
};

const insertColumns = (table, colIndex, amount) => {
  const newMatrix = [];

  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]];

    for (let k = 0; k < amount; k++) {
      newRow.splice(colIndex + k, 0, "");
    }

    newMatrix.push(newRow);
  }

  return newMatrix;
};

const deleteColumns = (table, colIndex, amount) => {
  const newTable = [];
  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]];
    newRow.splice(colIndex, amount);
    newTable.push(newRow);
  }

  return newTable;
};

const transpose = (array) => {
  const rows = array.length;
  const cols = array[0].length;
  const transposedArray = [];

  for (let j = 0; j < cols; j++) {
    transposedArray[j] = [];
  }

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      transposedArray[c][r] = array[r][c];
    }
  }

  return transposedArray;
};

const moveRowTable = (table, movedRows, finalIndex) => {
  let keyTable = table.map((item, key) => {
    return {
      index: key,
      row: item,
    };
  });

  let movedTable = table.filter(
    (item, key) => movedRows.includes(key) === true
  );
  let movedKeyTable = movedTable.map((item) => {
    return {
      index: "new",
      row: item,
    };
  });

  let left = keyTable.slice(0, finalIndex);
  let right = keyTable.slice(finalIndex);
  let newArrayKey = [...left, ...movedKeyTable, ...right];

  newArrayKey = newArrayKey.filter(
    (item) => movedRows.includes(item.index) === false
  );
  let newArray = newArrayKey.map((item) => item.row);

  return newArray;
};

const moveColumnTable = (table, movedColumns, finalIndex) => {
  let transposedTable = transpose(table);
  let movedTable = moveRowTable(transposedTable, movedColumns, finalIndex);
  return transpose(movedTable);
}

const SocketTable = () => {
  const location = useLocation();
  const [loginID, setLoginID] = useState("");

  const [tableData, setTableData] = useState(defaultData);
  const [rowHeights, setRowHeights] = useState(25);
  const [colWidths, setColWidths] = useState(60);

  const customOptions = {
    afterChange: function (changes, source) {
      // changes : 변경된 데이터 정보, source : 변경을 발생시킨 원인
      if (source === "loadData") return;
  
      console.log("Changed Data :", source, changes);
      socketIO.emit("changeData", changes);
    },
    afterCreateRow: function (index, amount) {
      console.log("create row :", index, amount);
  
      let rowHeights = getRowHeights(this);
      socketIO.emit("createRow", index, amount, rowHeights);
    },
    afterRemoveRow: function (index, amount) {
      console.log("remove row :", index, amount);
  
      let rowHeights = getRowHeights(this);
      socketIO.emit("removeRow", index, amount, rowHeights);
    },
    afterCreateCol: function (index, amount) {
      console.log("create col :", index, amount);
  
      let colWidths = getColWidths(this);
      socketIO.emit("createCol", index, amount, colWidths);
    },
    afterRemoveCol: function (index, amount) {
      console.log("remove col :", index, amount);
  
      let colWidths = getColWidths(this);
      socketIO.emit("removeCol", index, amount, colWidths);
    },
    afterRowMove: function (movedRows, finalIndex, dropIndex, movePossible, orderChanged) {
      console.log("move row", movedRows, finalIndex, dropIndex, movePossible, orderChanged);
  
      let rowHeights = getRowHeights(this);

      setRowHeights(rowHeights);
      setTableData(this.getData());  
      socketIO.emit("moveRow", movedRows, finalIndex, rowHeights);
    },
    afterColumnMove: function (movedColumns, finalIndex, dropIndex, movePossible, orderChanged) {
      console.log("move col", movedColumns, finalIndex, dropIndex, movePossible, orderChanged);
  
      let colWidths = getColWidths(this);

      setColWidths(colWidths);
      setTableData(this.getData());
      socketIO.emit("moveCol", movedColumns, finalIndex, colWidths);
    },
    afterRowResize: function (newSize, row, isDoubleClick) {
      console.log("resize row");
  
      let rowHeights = getRowHeights(this);
      socketIO.emit("resizeRow", rowHeights);
    },
    afterColumnResize: function (newSize, column, isDoubleClick) {
      console.log("resize col");
  
      let colWidths = getColWidths(this);
      socketIO.emit("resizeCol", colWidths);
    },
  };

  const init = () => {
    setLoginID(location.state.loginID);
  };

  useEffect(init, []);

  // const respondDataCallback = (newData) => {
  //   setTableData(newData);
  // };

  const resCreateRow = (index, amount, currentRowHeights) => {
    console.log("createRow", index, amount, currentRowHeights);
    setTableData((prev) => {
      let newTable = insertRows(prev, index, amount);
      return newTable;
    });

    setRowHeights(currentRowHeights);
  };

  const resRemoveRow = (index, amount, currentRowHeights) => {
    console.log("removeRow", index, amount, currentRowHeights);
    setTableData((prev) => {
      let newTable = deleteRows(prev, index, amount);
      return newTable;
    });

    setRowHeights(currentRowHeights);
  };

  const resCreateCol = (index, amount, currentColWidths) => {
    console.log("createCol", index, amount, currentColWidths);
    setTableData((prev) => {
      let newTable = insertColumns(prev, index, amount);
      return newTable;
    });

    setColWidths(currentColWidths);
  };

  const resRemoveCol = (index, amount, currentColWidths) => {
    console.log("removeCol", index, amount, currentColWidths);
    setTableData((prev) => {
      let newTable = deleteColumns(prev, index, amount);
      return newTable;
    });

    setColWidths(currentColWidths);
  };

  const resChangeData = (changes, currentRowHeights, currentColWidths) => {
    console.log("changeData", changes, currentRowHeights, currentColWidths);
    setTableData((prev) => {
      let newTable = [...prev];
      for (let change of changes) {
        let [row, col, before, after] = change;
        newTable[row][col] = after;
      }
      return newTable;
    });

    setRowHeights(currentRowHeights);
    setColWidths(currentColWidths);
  };

  const resMoveRow = (movedRows, finalIndex, currentRowHeights) => {
    console.log("resMoveRow", movedRows, finalIndex, currentRowHeights);
    setTableData((prev) => {
      let newTable = moveRowTable(prev, movedRows, finalIndex);
      return newTable;
    });
    
    setRowHeights(currentRowHeights);
  };

  const resMoveCol = (movedColumns, finalIndex, currentColWidths) => {
    console.log("resMoveCol", movedColumns, finalIndex, currentColWidths);
    setTableData((prev) => {
      console.log({prev})
      let newTable = moveColumnTable(prev, movedColumns, finalIndex);
      console.log({newTable});
      return newTable;
    });

    setColWidths(currentColWidths);
  };

  const resResizeRow = (rowHeights) => {
    console.log("resResizeRow", rowHeights);
    setRowHeights(rowHeights);
  };

  const resResizeCol = (colWidths) => {
    console.log("resResizeCol", colWidths);
    setColWidths(colWidths);
  };

  useEffect(() => {
    socketIO.connect();
    if (!socketIO) return;

    // socketIO.on("respondData", respondDataCallback);
    socketIO.on("resCreateRow", resCreateRow);
    socketIO.on("resRemoveRow", resRemoveRow);
    socketIO.on("resCreateCol", resCreateCol);
    socketIO.on("resRemoveCol", resRemoveCol);
    socketIO.on("resChangeData", resChangeData);
    socketIO.on("resMoveRow", resMoveRow);
    socketIO.on("resMoveCol", resMoveCol);
    socketIO.on("resResizeRow", resResizeRow);
    socketIO.on("resResizeCol", resResizeCol);

    return () => {
      // socketIO.off("respondData", respondDataCallback);
      socketIO.off("resCreateRow", resCreateRow);
      socketIO.off("resRemoveRow", resRemoveRow);
      socketIO.off("resCreateCol", resCreateCol);
      socketIO.off("resRemoveCol", resRemoveCol);
      socketIO.off("resChangeData", resChangeData);
      socketIO.off("resMoveRow", resMoveRow);
      socketIO.off("resMoveCol", resMoveCol);
      socketIO.off("resResizeRow", resResizeRow);
      socketIO.off("resResizeCol", resResizeCol);
    };
  }, []);

  useEffect(() => {
    socketIO.once("initData", (data) => {
      console.log(data);
      if (data.currentData) {
        setTableData(data.currentData);
        setRowHeights(data.currentRowHeights);
        setColWidths(data.currentColWidths);
      } else {
        let basicRowHeight = 25;
        let basicColWidth = 60;
        let initRowHeights = new Array(defaultData.length).fill(basicRowHeight);
        let initColWidths = new Array(defaultData[0].length).fill(
          basicColWidth
        );

        socketIO.emit("setCurrentData", tableData);
        socketIO.emit("setRowHeights", initRowHeights);
        socketIO.emit("setColWidths", initColWidths);
      }
    });
  }, []);

  return (
    <div>    
      <Conversation
        info="I'm fine, thank you, and you?"
        lastSenderName={loginID}
        name={loginID}
      >
        <Avatar name={loginID} src={AVATAR_MAP[loginID]} status="available" />
      </Conversation>
      <MessageSeparator style={{ marginTop: 5, marginBottom: 5 }} />
      <SimpleHandsOnTable
        data={tableData}
        rowHeights={rowHeights}
        colWidths={colWidths}
        customOptions={customOptions}
      />
    </div>
  );
};

export default SocketTable;

setter 정리

 

현재 data, rowHeights, colWidthsSimpleHandsOnTable 컴포넌트에 넘겨주고 있다.

  <SimpleHandsOnTable
    data={tableData}
    rowHeights={rowHeights}
    colWidths={colWidths}
    customOptions={customOptions}
  />

 

SimpleHandsOnTable는 해당 data가 변경되면 테이블을 갱신한다.

  useEffect(() => {
    makeTable();
  }, [data, rowHeights, colWidths]);

 

테이블을 갱신하는 useState 하나로 합치는 것이 수월하다.

  const [tableData, setTableData] = useState(defaultData);
  const [rowHeights, setRowHeights] = useState(25);
  const [colWidths, setColWidths] = useState(60);

 

3개의 값tableInfo로 합친다.

  const [tableInfo, setTableInfo] = useState({
    data : defaultData,
    rowHeights: 25,
    colWidths: 60,
  });

 

관련 코드를 setTableInfo로 변경한다. (나머지는 전체 코드 참고)

이전에 setTableDatasetRowHeights각각 호출했지만, 이제 setTableInfo로 한 번에 호출할 수 있다.

  const resCreateRow = (index, amount, currentRowHeights) => {
    console.log("createRow", index, amount, currentRowHeights);

    setTableInfo((prev) => {
      let newTable = insertRows(prev.data, index, amount);
      return {
        ...prev,
        data: newTable,
        rowHeights: currentRowHeights,
      }      
    });

    // setTableData((prev) => {
    //   let newTable = insertRows(prev, index, amount);
    //   return newTable;
    // });

    // setRowHeights(currentRowHeights);
  };

 

SimpleHandsOnTable의 옵션은 tableInfo props에 따라 아래와 같이 수정한다.

const SimpleHandsOnTable = ({
  tableInfo,
  setTable,
  customOptions,
}) => {

  ...

  const options = {
    data: tableInfo.data,
    ...
    colWidths: tableInfo.colWidths /* 특정 위치 너비 변경 : [60, 120, 60, 60, 60, 60, 60] */,
    rowHeights: tableInfo.rowHeights,
    ...
  };

 

SimpleHandsOnTableuseEffect가 간결하게 변한다.

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

전체 코드는 다음과 같다.

 

socketIOServer.js

const { Server } = require("socket.io");

let currentData = undefined;
let currentRowHeights = undefined;
let currentColWidths = undefined;

const io = new Server("3333", {
  cors: {
    origin: "http://localhost:3000",
  },
});

const insertRows = (table, rowIndex, amount) => {
  const afterIndex = table.slice(rowIndex);
  const emptyArray = Array.from({ length: table[0].length }, () => "");
  const newRows = Array.from({ length: amount }, () => emptyArray);  
  const newArray = table.slice(0, rowIndex).concat(newRows, afterIndex);

  return newArray;
};

const deleteRows = (table, rowIndex, amount) => {
  const newTable = [...table]; 

  newTable.splice(rowIndex, amount);

  return newTable;
};

const insertColumns = (table, colIndex, amount) => {
  const newMatrix = [];

  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]]; 

    for (let k = 0; k < amount; k++) {
      newRow.splice(colIndex + k, 0, "");
    }

    newMatrix.push(newRow); 
  }

  return newMatrix;
};

const deleteColumns = (table, colIndex, amount) => {
  const newTable = [];
  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]]; 
    newRow.splice(colIndex, amount); 
    newTable.push(newRow); 
  }

  return newTable;
};

const transpose = (array) => {
  const rows = array.length;
  const cols = array[0].length;
  const transposedArray = [];
  
  for (let j = 0; j < cols; j++) {
    transposedArray[j] = [];
  }

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      transposedArray[c][r] = array[r][c];
    }
  }

  return transposedArray;
}

const moveRowTable = (table, movedRows, finalIndex) => {
  let keyTable = table.map((item, key) => {
    return {
      index : key,
      row : item,
    }
  });

  let movedTable = table.filter((item, key) => movedRows.includes(key) === true);
  let movedKeyTable = movedTable.map((item) => {
    return {
      index : "new",
      row : item,
    }
  });
  
  let left = keyTable.slice(0, finalIndex);
  let right = keyTable.slice(finalIndex);
  let newArrayKey = [...left, ...movedKeyTable, ...right];

  newArrayKey = newArrayKey.filter((item) => movedRows.includes(item.index) === false);
  let newArray = newArrayKey.map(item => item.row);

  return newArray;
};

const moveColumnTable = (table, movedColumns, finalIndex) => {
  let transposedTable = transpose(table);
  let movedTable = moveRowTable(transposedTable, movedColumns, finalIndex);
  return transpose(movedTable);
}

io.sockets.on("connection", (socket) => {
  socket.emit("initData", {    
    currentData,
    currentRowHeights,
    currentColWidths,
  });

  socket.on("setCurrentData", (data) => {   
    console.log("setCurrentData");
    currentData = data; 
  });

  socket.on("createRow", (index, amount, rowHeights) => {    
    console.log("createRow", index, amount);
    currentData = insertRows(currentData, index, amount);
    currentRowHeights = rowHeights
    socket.broadcast.emit("resCreateRow", index, amount, currentRowHeights);
  });
  
  socket.on("removeRow", (index, amount, rowHeights) => {    
    console.log("removeRow", index, amount);
    currentData = deleteRows(currentData, index, amount);
    currentRowHeights = rowHeights
    socket.broadcast.emit("resRemoveRow", index, amount, currentRowHeights);
  });
  
  socket.on("createCol", (index, amount, colWidths) => {   
    console.log("createCol", index, amount); 
    currentData = insertColumns(currentData, index, amount);
    currentColWidths = colWidths;
    socket.broadcast.emit("resCreateCol", index, amount, currentColWidths);
  });
  
  socket.on("removeCol", (index, amount, colWidths) => {    
    console.log("removeCol", index, amount);
    currentData = deleteColumns(currentData, index, amount);
    currentColWidths = colWidths;
    socket.broadcast.emit("resRemoveCol", index, amount, currentColWidths);
  });
  
  socket.on("changeData", (changes) => {    
    console.log("changeData", changes);
    for(let change of changes) {
      let [row, col, before, after] = change;
      currentData[row][col] = after;
    }
    socket.broadcast.emit("resChangeData", changes, currentRowHeights, currentColWidths);
  });
  
  socket.on("moveRow", (movedRows, finalIndex, rowHeights) => {    
    console.log("moveRow", movedRows, finalIndex);
    currentData = moveRowTable(currentData, movedRows, finalIndex);
    currentRowHeights = rowHeights;
    socket.broadcast.emit("resMoveRow", movedRows, finalIndex, currentRowHeights);
  });  

  socket.on("moveCol", (movedColumns, finalIndex, colWidths) => {    
    console.log("moveCol", movedColumns, finalIndex);
    currentData = moveColumnTable(currentData, movedColumns, finalIndex);
    currentColWidths = colWidths;
    socket.broadcast.emit("resMoveCol", movedColumns, finalIndex, currentColWidths);
  });  

  socket.on("setRowHeights", (initRowHeights) => {    
    console.log("setRowHeights", initRowHeights);
    currentRowHeights = initRowHeights;
  });

  socket.on("setColWidths", (initColWidths) => {    
    console.log("setColWidths", initColWidths);
    currentColWidths = initColWidths;
  });  

  socket.on("resizeRow", (rowHeights) => {    
    console.log("resizeRow", rowHeights);
    currentRowHeights = rowHeights;
    socket.broadcast.emit("resResizeRow", rowHeights);
  });  

  socket.on("resizeCol", (colWidths) => {    
    console.log("resizeCol", colWidths);
    currentColWidths = colWidths;
    socket.broadcast.emit("resResizeCol", colWidths);
  });

  // socket.on("sendData", (data) => {    
  //   console.log(data);
  //   currentData = data;
  //   socket.broadcast.emit("respondData", currentData);
  // });

  socket.on("disconnect", () => {});
});

 

SocketTable.js (Client)

import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";

import styles from "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";

import { io } from "socket.io-client";

import {
  Avatar,
  Conversation,
  MessageSeparator,
} from "@chatscope/chat-ui-kit-react";

import SimpleHandsOnTable from "./SimpleHandsOnTable";

const AVATAR_MAP = {
  Lilly: "https://chatscope.io/storybook/react/assets/lilly-aj6lnGPk.svg",
  Joe: "https://chatscope.io/storybook/react/assets/joe-v8Vy3KOS.svg",
  Emily: "https://chatscope.io/storybook/react/assets/emily-xzL8sDL2.svg",
  Akane: "https://chatscope.io/storybook/react/assets/akane-MXhWvx63.svg",
  Eliot: "https://chatscope.io/storybook/react/assets/eliot-JNkqSAth.svg",
  Zoe: "https://chatscope.io/storybook/react/assets/zoe-E7ZdmXF0.svg",
};

const defaultData = [
  ["", "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],
];

let socketIO = io("http://localhost:3333", { autoConnect: false });

const getRowHeights = (handsOnTable) => {
  let countRows = handsOnTable.countRows();
  let rowHeights = [];
  for (let r = 0; r < countRows; r++)
    rowHeights.push(handsOnTable.getRowHeight(r));
  return rowHeights;
};

const getColWidths = (handsOnTable) => {
  let countCols = handsOnTable.countCols();
  let colWidths = [];
  for (let c = 0; c < countCols; c++)
    colWidths.push(handsOnTable.getColWidth(c));
  return colWidths;
};

const insertRows = (table, rowIndex, amount) => {
  const afterIndex = table.slice(rowIndex);
  const emptyArray = Array.from({ length: table[0].length }, () => "");
  const newRows = Array.from({ length: amount }, () => emptyArray);
  const newArray = table.slice(0, rowIndex).concat(newRows, afterIndex);

  return newArray;
};

const deleteRows = (table, rowIndex, amount) => {
  const newTable = [...table];

  newTable.splice(rowIndex, amount);

  return newTable;
};

const insertColumns = (table, colIndex, amount) => {
  const newMatrix = [];

  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]];

    for (let k = 0; k < amount; k++) {
      newRow.splice(colIndex + k, 0, "");
    }

    newMatrix.push(newRow);
  }

  return newMatrix;
};

const deleteColumns = (table, colIndex, amount) => {
  const newTable = [];
  for (let i = 0; i < table.length; i++) {
    const newRow = [...table[i]];
    newRow.splice(colIndex, amount);
    newTable.push(newRow);
  }

  return newTable;
};

const transpose = (array) => {
  const rows = array.length;
  const cols = array[0].length;
  const transposedArray = [];

  for (let j = 0; j < cols; j++) {
    transposedArray[j] = [];
  }

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      transposedArray[c][r] = array[r][c];
    }
  }

  return transposedArray;
};

const moveRowTable = (table, movedRows, finalIndex) => {
  let keyTable = table.map((item, key) => {
    return {
      index: key,
      row: item,
    };
  });

  let movedTable = table.filter(
    (item, key) => movedRows.includes(key) === true
  );
  let movedKeyTable = movedTable.map((item) => {
    return {
      index: "new",
      row: item,
    };
  });

  let left = keyTable.slice(0, finalIndex);
  let right = keyTable.slice(finalIndex);
  let newArrayKey = [...left, ...movedKeyTable, ...right];

  newArrayKey = newArrayKey.filter(
    (item) => movedRows.includes(item.index) === false
  );
  let newArray = newArrayKey.map((item) => item.row);

  return newArray;
};

const moveColumnTable = (table, movedColumns, finalIndex) => {
  let transposedTable = transpose(table);
  let movedTable = moveRowTable(transposedTable, movedColumns, finalIndex);
  return transpose(movedTable);
};

const SocketTable = () => {
  const location = useLocation();
  const [loginID, setLoginID] = useState("");

  const [tableInfo, setTableInfo] = useState({
    data: defaultData,
    rowHeights: 25,
    colWidths: 60,
  });

  const customOptions = {
    afterChange: function (changes, source) {
      // changes : 변경된 데이터 정보, source : 변경을 발생시킨 원인
      if (source === "loadData") return;

      console.log("Changed Data :", source, changes);
      socketIO.emit("changeData", changes);
    },
    afterCreateRow: function (index, amount) {
      console.log("create row :", index, amount);

      let rowHeights = getRowHeights(this);
      socketIO.emit("createRow", index, amount, rowHeights);
    },
    afterRemoveRow: function (index, amount) {
      console.log("remove row :", index, amount);

      let rowHeights = getRowHeights(this);
      socketIO.emit("removeRow", index, amount, rowHeights);
    },
    afterCreateCol: function (index, amount) {
      console.log("create col :", index, amount);

      let colWidths = getColWidths(this);
      socketIO.emit("createCol", index, amount, colWidths);
    },
    afterRemoveCol: function (index, amount) {
      console.log("remove col :", index, amount);

      let colWidths = getColWidths(this);
      socketIO.emit("removeCol", index, amount, colWidths);
    },
    afterRowMove: function (movedRows, finalIndex, dropIndex, movePossible, orderChanged) {
      console.log("move row", movedRows, finalIndex, dropIndex, movePossible, orderChanged);

      let rowHeights = getRowHeights(this);
      setTableInfo((prev) => {
        return {
          ...prev,
          data: this.getData(),
          rowHeights,
        };
      });

      socketIO.emit("moveRow", movedRows, finalIndex, rowHeights);
    },
    afterColumnMove: function (movedColumns, finalIndex, dropIndex, movePossible, orderChanged) {
      console.log("move col", movedColumns, finalIndex, dropIndex, movePossible, orderChanged);

      let colWidths = getColWidths(this);
      setTableInfo((prev) => {
        return {
          ...prev,
          data: this.getData(),
          colWidths,
        };
      });

      socketIO.emit("moveCol", movedColumns, finalIndex, colWidths);
    },
    afterRowResize: function (newSize, row, isDoubleClick) {
      console.log("resize row");

      let rowHeights = getRowHeights(this);
      socketIO.emit("resizeRow", rowHeights);
    },
    afterColumnResize: function (newSize, column, isDoubleClick) {
      console.log("resize col");

      let colWidths = getColWidths(this);
      socketIO.emit("resizeCol", colWidths);
    },
  };

  const init = () => {
    setLoginID(location.state.loginID);
  };

  useEffect(init, []);

  // const respondDataCallback = (newData) => {
  //   setTableData(newData);
  // };

  const resCreateRow = (index, amount, currentRowHeights) => {
    console.log("createRow", index, amount, currentRowHeights);

    setTableInfo((prev) => {
      let newTable = insertRows(prev.data, index, amount);
      return {
        ...prev,
        data: newTable,
        rowHeights: currentRowHeights,
      };
    });
  };

  const resRemoveRow = (index, amount, currentRowHeights) => {
    console.log("removeRow", index, amount, currentRowHeights);

    setTableInfo((prev) => {
      let newTable = deleteRows(prev.data, index, amount);
      return {
        ...prev,
        data: newTable,
        rowHeights: currentRowHeights,
      };
    });
  };

  const resCreateCol = (index, amount, currentColWidths) => {
    console.log("createCol", index, amount, currentColWidths);

    setTableInfo((prev) => {
      let newTable = insertColumns(prev.data, index, amount);
      return {
        ...prev,
        data: newTable,
        colWidths: currentColWidths,
      };
    });
  };

  const resRemoveCol = (index, amount, currentColWidths) => {
    console.log("removeCol", index, amount, currentColWidths);

    setTableInfo((prev) => {
      let newTable = deleteColumns(prev.data, index, amount);
      return {
        ...prev,
        data: newTable,
        colWidths: currentColWidths,
      };
    });
  };

  const resChangeData = (changes, currentRowHeights, currentColWidths) => {
    console.log("changeData", changes, currentRowHeights, currentColWidths);

    setTableInfo((prev) => {
      let newTable = [...prev.data];
      for (let change of changes) {
        let [row, col, before, after] = change;
        newTable[row][col] = after;
      }

      return {
        ...prev,
        data: newTable,
        rowHeights: currentRowHeights,
        colWidths: currentColWidths,
      };
    });
  };

  const resMoveRow = (movedRows, finalIndex, currentRowHeights) => {
    console.log("resMoveRow", movedRows, finalIndex, currentRowHeights);

    setTableInfo((prev) => {
      let newTable = moveRowTable(prev.data, movedRows, finalIndex);
      return {
        ...prev,
        data: newTable,
        rowHeights: currentRowHeights,
      };
    });
  };

  const resMoveCol = (movedColumns, finalIndex, currentColWidths) => {
    console.log("resMoveCol", movedColumns, finalIndex, currentColWidths);

    setTableInfo((prev) => {
      let newTable = moveColumnTable(prev.data, movedColumns, finalIndex);
      return {
        ...prev,
        data: newTable,
        colWidths: currentColWidths,
      };
    });
  };

  const resResizeRow = (rowHeights) => {
    console.log("resResizeRow", rowHeights);
    setTableInfo((prev) => {
      return {
        ...prev,
        rowHeights,
      };
    });
  };

  const resResizeCol = (colWidths) => {
    console.log("resResizeCol", colWidths);
    setTableInfo((prev) => {
      return {
        ...prev,
        colWidths,
      };
    });
  };

  useEffect(() => {
    socketIO.connect();
    if (!socketIO) return;

    // socketIO.on("respondData", respondDataCallback);
    socketIO.on("resCreateRow", resCreateRow);
    socketIO.on("resRemoveRow", resRemoveRow);
    socketIO.on("resCreateCol", resCreateCol);
    socketIO.on("resRemoveCol", resRemoveCol);
    socketIO.on("resChangeData", resChangeData);
    socketIO.on("resMoveRow", resMoveRow);
    socketIO.on("resMoveCol", resMoveCol);
    socketIO.on("resResizeRow", resResizeRow);
    socketIO.on("resResizeCol", resResizeCol);

    return () => {
      // socketIO.off("respondData", respondDataCallback);
      socketIO.off("resCreateRow", resCreateRow);
      socketIO.off("resRemoveRow", resRemoveRow);
      socketIO.off("resCreateCol", resCreateCol);
      socketIO.off("resRemoveCol", resRemoveCol);
      socketIO.off("resChangeData", resChangeData);
      socketIO.off("resMoveRow", resMoveRow);
      socketIO.off("resMoveCol", resMoveCol);
      socketIO.off("resResizeRow", resResizeRow);
      socketIO.off("resResizeCol", resResizeCol);
    };
  }, []);

  useEffect(() => {
    socketIO.once("initData", (data) => {
      console.log(data);
      if (data.currentData) {
        setTableInfo({
          data: data.currentData,
          rowHeights: data.currentRowHeights,
          colWidths: data.currentColWidths,
        });
      } else {
        let basicRowHeight = 25;
        let basicColWidth = 60;
        let initRowHeights = new Array(defaultData.length).fill(basicRowHeight);
        let initColWidths = new Array(defaultData[0].length).fill(basicColWidth);

        socketIO.emit("setCurrentData", defaultData);
        socketIO.emit("setRowHeights", initRowHeights);
        socketIO.emit("setColWidths", initColWidths);
      }
    });
  }, []);

  return (
    <div>
      <Conversation
        info="I'm fine, thank you, and you?"
        lastSenderName={loginID}
        name={loginID}
      >
        <Avatar name={loginID} src={AVATAR_MAP[loginID]} status="available" />
      </Conversation>
      <MessageSeparator style={{ marginTop: 5, marginBottom: 5 }} />
      <SimpleHandsOnTable tableInfo={tableInfo} customOptions={customOptions} />
    </div>
  );
};

export default SocketTable;

 

SimpleHandsOnTable.js

/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from "react";

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

import styled from "styled-components";

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

const SimpleHandsOnTable = ({
  tableInfo,
  setTable,
  customOptions,
}) => {
  const [displayCellInfo, setDisplaySetInfo] = useState("");
  const [selectedCell, setSelectedCell] = useState([0, 0]);

  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: tableInfo.data,
    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에 추가 */,
    className: "htMiddle htCenter" /* Cell Alignment */,
    width: 1000,
    height: 1000,
    startCols: 5 /* data가 없는 경우 기본 설정 */,
    startRows: 3 /* data가 없는 경우 기본 설정 */,
    afterSelection: cellSelected,
    colWidths: tableInfo.colWidths /* 특정 위치 너비 변경 : [60, 120, 60, 60, 60, 60, 60] */,
    rowHeights: tableInfo.rowHeights,
    //mergeCells: [],
    licenseKey: "non-commercial-and-evaluation",
  };

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

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

    myTable.render();
    if (setTable) setTable(myTable);
  };

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

  return (
    <div>
      <Box sx={{ m: 2 }}>
        <DisplayCellStyle>
          <span>{displayCellInfo}</span>
        </DisplayCellStyle>
        <div id="hot-app" style={{ marginTop: "13px" }}></div>
      </Box>
    </div>
  );
};

export default SimpleHandsOnTable;
반응형

댓글