참고
- https://handsontable.com/docs/javascript-data-grid/api/hooks/#afterrowmove
- 스프레드시트 공동 작업하기 Project Settings
- Handsontable에 Socket.IO 적용하기
- 변경된 셀만 데이터 전송해서 최적화하기
- 행 / 열 이동 및 크기 변경 연동하기
- 다른 클라이언트가 선택한 셀 표시하기
이제 행이나 열을 이동하거나, 삭제, 추가 그리고 크기 변경도 연동해 보자.
행 / 열 이동, 삭제, 추가 구현
afterRow / ColumnMove에는 여러 parameter가 있지만,
여기서는 movedRows / Columns와 finalIndex만 사용하는 경우만 처리한다.
먼저, movedRows와 finalIndex로 실제 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);
}
이제 위의 메서드를 서버와 클라이언트에 모두 추가한다.
서버는 현재 data를 moveRowTable과 moveColumnTable을 이용해서 갱신한다.
그리고 movedRows / Columns와 finalIndex를 클라이언트로 전송한다.
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);
});
클라이언트는 movedRows와 finalIndex를 서버로 전송하면 된다.
그리고 setTableData로 현재 data를 갱신해야 한다.
setTableData를 사용하기 위해 customOptions를 SocketTable 내부로 옮겼다.
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는 전체 값을 통째로 연동한다.
먼저 서버는 현재 row나 col의 size를 가지고 있어야하고, setRowHeights와 setColWidths로 초기화한다.
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",
};
SimpleHandsOnTable의 props로 rowHeights와 colWidths를 추가한다.
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, colWidths를 SimpleHandsOnTable 컴포넌트에 넘겨주고 있다.
<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로 변경한다. (나머지는 전체 코드 참고)
이전에 setTableData와 setRowHeights를 각각 호출했지만, 이제 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,
...
};
SimpleHandsOnTable도 useEffect가 간결하게 변한다.
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;
댓글