본문 바로가기
개발/React

React Handsontable로 csv 편집기 만들기 (13)

by 피로물든딸기 2021. 6. 9.
반응형

프로젝트 전체 링크

 

이전 - (12) table의 dummy 행과 열을 추가하기

현재 - (13) Download CSV

다음 - (14) handsontable methods

 

참고 

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

 

깃허브에서 코드 확인하기


수정한 csv를 다운로드 할 수 있도록 아래의 button을 추가하자.

handsontable의 exportPlugin을 이용하면 download가 가능하다.

//MyTable.js

...

const csvDownLoad = () => {
  const exportPlugin = myTable.getPlugin("exportFile");
  exportPlugin.downloadFile("csv", { filename: "MyFile" });
};

...

const MyTable = ({ csvFile }) => {
  useEffect(() => {
    init(csvFile);
  }, [csvFile]);

  return (
    <div>
      <button onClick={csvDownLoad}>DOWNLOAD</button>
      <div id="hot-app">
      </div>
    </div>
  );
};

 

DOWNLOAD 버튼을 클릭하면 아래와 같이 file이 download 된다.

 

메모장으로 csv file을 열어보면 아래와 같은 문제점을 알 수 있다.

csv File을 보여줄 때, height와 width에 dummy를 넣어줬지만,

실제 편집을 할 때, dummy에 값을 입력하지 않았으므로, dummy는 필요가 없다.

그러므로 편집된 csv에서 필요한 만큼만 download 하도록 변경해야 한다.

 

따라서 자바스크립트에서 사용하는 방식을 이용해여 download를 구현 해보자.


library.js에 downLoadCsv 함수를 만든다.

download할 data는 csv,이고 charset은 utf-8로 한다.

contents와 fileName만 적절히 넘겨주면 download 코드는 더 수정할 것이 없다.

// library.js

export const downLoadCsv = (contents, fileName = "MyFile.csv") => {
  let fileDown = "data:csv;charset=utf-8," + contents;

  let encodedUri = encodeURI(fileDown);
  let link = document.createElement("a");

  link.setAttribute("href", encodedUri);
  link.setAttribute("download", fileName);

  document.body.appendChild(link);

  link.click();

  document.body.removeChild(link);
};

fileName은 default로 MyFile.csv로 정의하였다.


다시 MyTable.js에서 csv를 upload 한 후, myTable을 log로 출력해보자.

//MyTable.js

const csvDownLoad = () => {
  console.log(myTable);
  ...
}

 

여러 method 들이 포함되어 있다.

이 중에서 getData를 이용하면, table을 필요한 만큼만 가져올 수 있다.

아래와 같이 코드를 수정한 후, file을 upload 하고 DOWNLOAD 버튼을 눌러보자.

const csvDownLoad = () => {
  console.log(myTable); 
  console.log(myTable.getData(0, 0, 2, 2));
  
  return;
};

 

getData를 이용해, myTable의 일부분 (0, 0) ~ (2, 2) 까지의 값들만 2차원 배열로 가져온 것을 알 수 있다.

따라서 getData를 이용하여 유효한 범위의 csv File만 download 하도록 해보자.


왼쪽의 table은 3 x 5의 csv가 된다.

오른쪽의 경우는 F와 G에 빈 값이더라도, H에 값이 있으므로 4 x 8의 csv가 된다.

그러므로 전체 table의 크기 (row, column)을 구한 후, data의 유효성을 판단한다.

 

myTable의 method countRowscountCols를 이용하면 전체 Row와 Column의 길이를 알 수 있다.

따라서 유효한 cell 중 가장 큰 Row와 Column의 길이로 getData에 값을 넣어준다.

 

먼저 tmpTables에 myTable (dummy 포함)을 모두 가져오자.

let rows = myTable.countRows();
let cols = myTable.countCols();
let tmpTables = myTable.getData(0, 0, rows - 1, cols - 1);

 

유효한 최대 길이 row와 col은 아래의 방법대로 구한다.

handsontable의 최초 빈칸에는 "" 이지만, 값을 지우면 null로 변한다. (log로 직접 확인해보자.)

따라서 row, col의 최댓값부터 값을 줄이면서 "" 또는 null이 아닌 곳 중 가장 큰 값을 저장한다.

maxCol = 0;
for(let r = 0; r < rows; r++) {
  for(let c = cols - 1; c >=0; c--) {
    if(!(tmpTables[r][c] === "" || tmpTables[r][c] === null)) {
      maxCol = (maxCol < c) ? c : maxCol;
      break;
    }
  }
}

maxRow = 0;
for(let c = 0; c < cols; c++) {
  for(let r = rows - 1; r >=0; r--) {
    if(!(tmpTables[r][c] === "" || tmpTables[r][c] === null)) {
      maxRow = (maxRow < r) ? r : maxRow;
      break;
    }
  }
}

 

유효한 값을 구했으므로 다시 getData를 이용해 table을 구한다.

그리고 downLoadCsv에 값을 넘겨준다.

let realTable = myTable.getData(0, 0, maxRow, maxCol);

lib.downLoadCsv(realTable);

 

아래와 같은 결과를 볼 수 있다.

 

문제는 realTable은 2차원 배열이고, 이것을 그대로 downLoadCsv에 넘겨줬기 때문이다.

따라서 map과 join을 이용하여 각 원소를 ","로 합치고, 각 line을 "\n"로 합쳐야 한다.

아래와 같이 코드를 수정하자.

let realTable = myTable.getData(0, 0, maxRow, maxCol).map((item) => item.join(",")).join("\n");

lib.downLoadCsv(realTable);

map / join 동작 참고

 

정상적으로 csv file이 반영된 것을 알 수 있다. (4 x 7의 csv file이 되었다.)


하지만 csv파일을 parsing할 때, "," 가 있는 경우는 ""으로 구분하였다.

따라서 각 cell, 원소에 ","가 있는지 확인하여, 양 옆에 ""를 추가한다.

(그렇게 하지 않으면 어떻게 되는지, cell에 ","를 넣어서 다시 upload 해보자.)

 

a → "a, b, c, d"가 하나의 cell이지만, 여러 개의 cell로 분리된 것을 볼 수 있다.

 

따라서 ","가 포함된 경우에 대해 방어코드를 추가한다.


문자 ","가 포함되었는지 여부는 includes를 이용한다.

 

realTable을 join하기 전에, 각 원소를 map으로 순회하면서 ","가 있는 경우는 ""를 양 옆에 추가한다.

let parsing = myTable.getData(0, 0, maxRow, maxCol)
              .map((item) => item.map((cell) => cell.includes(",") ? `"${cell}"` : `${cell}`));
let realTable = parsing.map((item) => item.join(",")).join("\n");

lib.downLoadCsv(realTable);

 

하지만 cell이 null/undefined인 경우에는 includes를 사용할 수 없다.

따라서 cell이 null/undefined인 경우 ""를 return 하도록 변경한다.

const getCell = (cell) => {
  if(cell === null || cell === undefined) return ``;
  return cell.includes(",") ? `"${cell}"` : `${cell}`;
}

const csvDownLoad = () => {
  ...
  let parsing = myTable.getData(0, 0, maxRow, maxCol)
                .map((item) => item.map((cell) => getCell(cell)));
  let realTable = parsing.map((item) => item.join(",")).join("\n");

  lib.downLoadCsv(realTable);

  return;
};

 

이제 cell에 ","가 정상 반영된 것을 알 수 있다.

 

csvDownLoad의 최종 코드는 아래와 같다.

//MyTable.js

...

const getCell = (cell) => {
  if(cell === null || cell === undefined) return ``;
  return cell.includes(",") ? `"${cell}"` : `${cell}`;
}

const csvDownLoad = () => {
  let rows = myTable.countRows();
  let cols = myTable.countCols();
  let tmpTables = myTable.getData(0, 0, rows - 1, cols - 1);
  let maxRow, maxCol;
  
  maxCol = 0;
  for(let r = 0; r < rows; r++) {
    for(let c = cols - 1; c >=0; c--) {
      if(!(tmpTables[r][c] === "" || tmpTables[r][c] === null)) {
        maxCol = (maxCol < c) ? c : maxCol;
        break;
      }
    }
  }

  maxRow = 0;
  for(let c = 0; c < cols; c++) {
    for(let r = rows - 1; r >=0; r--) {
      if(!(tmpTables[r][c] === "" || tmpTables[r][c] === null)) {
        maxRow = (maxRow < r) ? r : maxRow;
        break;
      }
    }
  }

  let parsing = myTable.getData(0, 0, maxRow, maxCol)
                .map((item) => item.map((cell) => getCell(cell)));
  let realTable = parsing.map((item) => item.join(",")).join("\n");

  lib.downLoadCsv(realTable);

  return;
};

 

// library.js

export const downLoadCsv = (contents, fileName = "MyFile.csv") => {
  let fileDown = "data:csv;charset=utf-8," + contents;

  let encodedUri = encodeURI(fileDown);
  let link = document.createElement("a");

  link.setAttribute("href", encodedUri);
  link.setAttribute("download", fileName);

  document.body.appendChild(link);

  link.click();

  document.body.removeChild(link);
};

이전 - (12) table의 dummy 행과 열을 추가하기

다음 - (14) handsontable methods

반응형

댓글