본문 바로가기
개발/Node JS

Node js, React 파일 관리 시스템 만들기 (1)

by 피로물든딸기 2021. 7. 3.
반응형

프로젝트 전체 링크

 

현재 - (1) Project Setting

다음 - (2) fetch로 GET 요청하고 응답 받기

 

깃허브에서 코드 확인하기


React Handsontable에 서버에서 파일을 불러오는 시스템을 만들어보자.

 

사용하는 node, npm ver은 아래와 같다.

$ node -v
v12.13.0

$ npm -v
6.12.0

 

handsontable-csv의 상위 폴더 React와 동일한 위치에 server 폴더를 만든다.

server.js가 node의 main이다.

그리고 log를 관리할 폴더와 router를 관리할 폴더를 미리 만들어둔다.

server.js에서 간단히 로그를 찍어보자.

//server.js

console.log("hello node js");

 

실행시 출력되는 것을 알 수 있다.

$ node server.js
hello node js

 

express를 이용하여 서버를 만든다. npm을 이용해 설치한다.

npm install --save express

 

다시 아래와 같이 코드를 바꾼다.

//server.js

const express = require('express');
const app = express();

app.listen(3002, () => console.log('Node.js Server is running on port 3002...'));

 

아래와 같이 서버가 실행되었다. 

(윈도우에서 git bash를 이용하여 실행하였다.)

$ node server.js
Node.js Server is running on port 3002...

React Handsontable에서 사용하였던 react 코드들은 아래와 같다.

node js, express로 폴더, 파일을 불러와서 csv를 불러올 수 있도록 할 예정이다.

//index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import {Provider} from "react-redux";
import store from './components/store';

ReactDOM.render(
  <Provider store={store}> 
    <App />
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

//App.js
import React, { useState } from "react";

import FileUpload from "./components/FileUpload";
import MyTable from "./components/MyTable";

const csvObjectDefault = {
  HEIGHT: 0,
  WIDTH: 0,
  csv: [],
};

const App = () => {
  const [csvObject, setCsvObject] = useState(csvObjectDefault);
  return (
    <div>
      <button onClick={() => console.log(csvObject)}>print csv</button>
      <div className="App">
        <FileUpload setCsvObject={setCsvObject} />
        <MyTable csvFile={csvObject}/>
      </div>
    </div>
  );
};

export default App;

 

// FileUpload.js
import React, { useState } from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";
import * as lib from "./library.js";
import "../css/FileUpload.scss";

const FileUpload = ({ setCsvObject, fileUploadFlag, flagOn, flagOff }) => {
  const [dropFlag, setDropFlag] = useState(false);

  const onClickFileLoad = () => {
    let my_input = document.getElementById("my-input");
    my_input.click();
  };

  return (
    <div>
      {fileUploadFlag && <button onClick={onClickFileLoad}>불러오기</button>}
      <input
        id="my-input"
        style={{ visibility: "hidden" }}
        type="file"
        accept=".csv"
        onChange={(e) => lib.handleUpload(e, setCsvObject, flagOn)}
      />
      {!fileUploadFlag && (
        <div
          id="drag-drop-field"
          className={dropFlag ? "in" : ""}
          onDrop={(e) => lib.handleOnDrop(e, setDropFlag, setCsvObject, flagOn)}
          onDragOver={(e) => lib.handleDragOver(e, setDropFlag)}
          onDragLeave={(e) => lib.handleOnDragLeave(e, setDropFlag)}
        >
          <p>drag & drop</p>
          <input
            type="file"
            accept=".csv"
            onChange={(e) => lib.handleUpload(e, setCsvObject, flagOn)}
          />
        </div>
      )}
    </div>
  );
};

function mapStateToProps(state, ownProps) {
  //console.log(state);
  return { fileUploadFlag: state };
}

function mapDispatchToProps(dispatch, ownProps) {
  return {
    flagOn: () => dispatch(actionCreators.flagOn()),
    flagOff: () => dispatch(actionCreators.flagOff()),
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(FileUpload);

 

//MyTable.js
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import * as lib from "./library.js";

import "handsontable/dist/handsontable.full.css";
import Handsontable from "handsontable";

let myTable;
let currentRow, currentColumn;

const getCell = (cell) => {
  if(cell === null) 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;
};

const MyTable = ({ csvFile, fileUploadFlag }) => {
  const [displayIndex, setDisplayIndex] = useState("");
  const [displayCell, setDisplayCell] = useState("");
  
  const selectCell = () => {
    let selected = myTable.getSelectedLast();

    currentRow = selected[0];
    currentColumn = selected[1];

    if(currentRow < 0 || currentColumn < 0) return;

    setDisplayCell(myTable.getValue());
    setDisplayIndex(`${lib.rowToAlpha(currentColumn + 1)}${currentRow + 1}`);
  }

  const setValueCell = (e) => {
    if(currentRow < 0 || currentColumn < 0) return;

    setDisplayCell(e.target.value); 
    myTable.setDataAtCell(currentRow, currentColumn, e.target.value);
  }

  const init = (csvFile) => {
    if (csvFile === undefined || csvFile.HEIGHT === 0) return;
  
    const container = document.getElementById("hot-app");
  
 

    myTable = new Handsontable(container, {
      data: lib.makeTable(csvFile, 2, 3),
      colHeaders: true,         /* column header는 보이게 설정 */
      rowHeaders: true,         /* row header 보이게 설정 */
      colWidths: [60, 60, 60, 60, 60, 60, 60],
      wordWrap: false,          /* 줄 바꿈 x */
      width: "50%",
      manualColumnResize: true, /* column 사이즈 조절 */
      manualRowResize: true,    /* row 사이즈 조절 */
      manualColumnMove: true,   /* column move 허용 */
      manualRowMove: true,      /* row move 허용 */
      dropdownMenu: true,       /* dropdown 메뉴 설정 */
      filters: true,            /* 필터 기능 on */
      contextMenu: true,        /* cell 클릭 시 메뉴 설정 */
      licenseKey: "non-commercial-and-evaluation",
      afterSelection: selectCell,
    });
  };
  
  useEffect(() => {
    init(csvFile);
  }, [csvFile]);

  return (
    <div>
      {fileUploadFlag && (
        <div>
          <button onClick={csvDownLoad}>DOWNLOAD</button>
          <div>
            <span>{displayIndex}</span>
            <input value={displayCell} onChange={setValueCell} />
          </div>
          <div id="hot-app"></div>
        </div>
      )}
    </div>
  );
};

function mapStateToProps(state, ownProps) {
  //console.log(state);
  return { fileUploadFlag: state };
}

export default connect(mapStateToProps)(MyTable);

 

//store.js

import { createStore } from "redux";

const FLAG_ON = "FLAG_ON";
const FLAG_OFF = "FLAG_OFF";

const flagOn = () => {
  return {
    type: FLAG_ON,
  };
}

const flagOff = () => {
  return {
    type: FLAG_OFF,
  };
}

const reducer = (state = false, action) => {
  switch (action.type) {
    case FLAG_ON: /* File Upload 성공 */
      return true;
    case FLAG_OFF: /* File Upload 전 */
      return false;
    default:
      return state;
  }
};

/* subscribe */
const store = createStore(reducer);

export const actionCreators = {
  flagOn,
  flagOff
}

export default store;

 

// library.js

const DELIMITER = ',';
const APOSTROPHE = '"';

export const handleOnDragLeave = (e, setState) => {
  e.preventDefault();
  setState(false);
  return false;
};

export const handleDragOver = (e, setState) => {
  e.preventDefault();
  setState(true);
  return false;
};

export const handleOnDrop = (e, setState, setCsvObject, flagOff) => {
  e.preventDefault();

  let file = e.dataTransfer.files[0];
  let fileReader = new FileReader();

  fileReader.readAsText(file, "utf-8"); // or euc-kr

  fileReader.onload = function () {
    //console.log(fileReader.result); 
    parsingCsv(fileReader.result, setCsvObject);
    return;
  };

  setState(false);
  flagOff();
  return false; 
};

export const handleUpload = (e, setCsvObject, flagOff) => {
  let file = e.target.files[0];
  let fileReader = new FileReader();
  
  if(file === undefined) return; /* 방어 코드 추가 */

  fileReader.readAsText(file, "utf-8"); // or euc-kr

  fileReader.onload = function () {
    //console.log(fileReader.result); 
    parsingCsv(fileReader.result, setCsvObject);
  };
  flagOff();
}

export const mySplit = (line, delimiter, ignore) => {
  let spt = [];
  let tmp = "";
  let flag = false;

  for(let i = 0; i < line.length; i++)
  {
    if(ignore === line[i] && flag === true) 
    {
      flag = false;
      continue;
    }
    else if(ignore === line[i])
    {
      flag = true;
      continue;
    } 
    
    if(line[i] === delimiter && flag === false) {
      spt.push(tmp);
      tmp = "";

      continue;
    }

    tmp += line[i];
  }

  spt.push(tmp);

  return spt;
}

export const parsingCsv = (file, setCsvObject) => {
  let height, width;
  let obj = {
    HEIGHT: 0,
    WIDTH: 0,
    csv: [],
  };

  obj.csv = [];

  let sptLine = file.split(/\r\n|\n/);
  
  let maxLength = 0;
  for(let line of sptLine) {
    let spt = mySplit(line, DELIMITER, APOSTROPHE);
    if(maxLength < spt.length) maxLength = spt.length;
  }
  
  height = 0;
  for(let line of sptLine)
  {
    if(line === "") continue;

    let spt = mySplit(line, DELIMITER, APOSTROPHE);
    let pushCount = maxLength - spt.length;
    for(let i = 0; i < pushCount; i++) spt.push("");

    obj.csv.push(spt);
    height++;
  }

  width = obj.csv[0].length;

  obj.HEIGHT = height;
  obj.WIDTH = width;

  setCsvObject(obj);

  return;
}

export const makeTable = (csvObject, height, width) => {
  let table = [];
    
  for(let h = 0; h < csvObject.HEIGHT; h++)
  {
    let line = [];
    for(let w = 0; w < csvObject.WIDTH; w++) line.push(csvObject.csv[h][w]);
    for(let w = 0; w < width; w++) line.push("");

    table.push(line);
  }

  for(let h = 0; h < height; h++) 
  {
    let dummy = [];
    for(let w = 0; w < csvObject.WIDTH + width; w++) dummy.push("");
    table.push(dummy);
  }

  return table;
}

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

export const rowToAlpha = (row) => {
  const numToAlpha = [
    "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
    "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
    "U", "V", "W", "X", "Y", "Z",
  ];

  if (row <= 26) return numToAlpha[row - 1];

  if (26 < row && row <= 26 * 26) {
    let front = numToAlpha[parseInt(row / 26) - 1];
    let back = numToAlpha[(row % 26) - 1];

    if (row % 26 === 0) {
      back = "Z";
      front = numToAlpha[parseInt(row / 26) - 2];
    }

    return front + back;
  }

  return "too_long";
}

 

// DragDropTest.scss
#drag-drop-field { 
    border: 3px dashed blue; 
    width: 70vw; 
    height: 70vh; 
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    p {
      font-size: 30px;
    }
    
    &.in { 
      border: 3px dashed red;  
    }
  }
  

 

// FileUpload.scss
#drag-drop-field { 
    border: 3px dashed blue; 
    width: 70vw; 
    height: 70vh; 
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    p {
      font-size: 30px;
    }
    
    &.in { 
      border: 3px dashed red;  
    }
  }
  


다음 - (2) fetch로 GET 요청하고 응답 받기

반응형

댓글