본문 바로가기
개발/Node JS

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

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

프로젝트 전체 링크

 

이전 - (13) shelljs를 이용하여 server의 파일 삭제하기

현재 - (14) multer를 이용하여 여러 개의 파일 업로드하기

 

깃허브에서 코드 확인하기


지금까지 서버에 파일을 하나씩 저장하고 삭제하였다.

이제 여러 개의 파일을 한꺼번에 업로드 하는 기능을 만들어보자.

 

파일을 upload 하기 위해 multer를 설치한다.

npm install multer

 

server에 fileUpload.js를 만든다.

multer를 사용하기 위해 storage와 limits를 설정한다. 

 

storage에서 destination에 path를 넘겨주면 저장할 위치를 설정할 수 있다.

그리고 fileName은 이름을 설정한다. 여기서는 전달된 파일 그대로 저장한다.

 

limits는 업로드할 파일의 크기를 제한 할 수 있다.

 

multer.any()로 하면 전달된 모든 파일을 허용한다.

// fileUpload.js
const express = require("express");
const router = express.Router();
const multer = require("multer"); // npm install multer

router.post("/", (req, res) => {
  let path = req.query.path;
  const storage = multer.diskStorage({
    destination: (req, file, callback) => {
        callback(null, `${path}`);	//업로드 파일의 저장 위치를 설정
    },
    filename: (req, file, callback) => {
        callback(null, `${file.originalname}`);	// 파일이 저장될 때 이름 설정
    },
  });
  
  const limits = {
    files: 50,
    fileSize: 1024 * 1024 * 1024, //1G
  }

  const upload = multer({ storage, limits }).any();

  const reqFiles = [];

  upload(req, res, (err) => {
    if (err) {
      return res.json({ success: false, err });
    }

    for(let i = 0; i < req.files.length; i++) {
      reqFiles.push(req.files[i].fileName);
    }

    return res.json({
      success: true,
      //url: res.req.file.path,
      fileName: reqFiles,
    });
  });

  return;
 });
 
module.exports = router;

 

그리고 server.js에 fileUpload를 추가한다.

//server.js

...

const fileDelete = require('./routes/fileDelete');
const fileUpload = require('./routes/fileUpload');

...

app.use('/fileDelete', fileDelete);
app.use('/fileUpload', fileUpload);

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

React의 nodelibrary.js에서 axios/post를 이용해 파일을 전달한다.

파일을 전달하기 위해서 FormData 인터페이스를 사용한다.

uploadFiles에 넘겨 받은 file의 목록을 formData에 추가하여, post로 넘기면 된다.

upload가 성공/실패 여부에 따라 alert를 띄워주고, 성공할 경우 callback 함수를 실행하자.

여기서도 fileList를 갱신할 필요가 있으므로 callback함수에서는 getFileList를 실행할 것이다.

//nodelibrary.js
import axios from "axios";

...

export function uploadFiles(path, fileList, callback) {
  let formData = new FormData();
  
  const config = {
    header: { "content-type": "multipart/form-data" },
  };

  for(let i = 0; i < fileList.length; i++) formData.append("file", fileList[i]);

  axios
    .post(`${MY_SERVER}/fileUpload?path=${path}`, formData, config)
    .then((response) => {
      console.log(response);
      if (response.data.success) {
        console.log(response.data);
        alert(`${response.data.fileName.length}개 파일 업로드 성공`)
        if(callback) callback();
      } else {
        alert("업로드에 실패했습니다.");
      }
    });
}

 

MyTable.js에서 UPLOAD 버튼을 추가한다. 

버튼으로만 추가하고 싶기 때문에 실제 파일 업로드는 hidden으로 숨겨둔다.

관련 설명은 링크를 참고하자.

input에 multiple option을 주면 여러 개의 파일을 선택할 수 있다.

그리고 accept에는 csv 파일만 올릴 수 있도록 하자.

//MyTable.js

<button onClick={deleteFile}>DELETE</button>
<button onClick={onClickUpload}>UPLOAD</button>

...

<input
  type="file"
  id="hidden-upload"
  style={{ visibility: "hidden" }}
  accept=".csv"
  multiple
  onChange={(e) => uploadFiles(e)}
/>
<div id="hot-app"></div>

 

UPLOAD button에 연결된 onClickUpload는 경로가 제대로 선택된 경우, input을 click하도록 하면 된다.

const onClickUpload = () => {
  if(pathInfo.version === "" || pathInfo.country === "") {
    alert("version / country를 모두 선택하세요.");
    return;
  }

  document.getElementById("hidden-upload").click();
}

 

hidden input을 클릭할 때는 uploadFiles가 실행된다.

 

e.target.files에는 업로드할 파일들의 목록이 넘어온다.

objFileList로 fileList를 object로 만든 후, 업로드할 파일과 중복된 이름이 있는지 체크한다.

중복이 있을 때, 덮어쓸지 confirm을 이용하여 물어보고 덮어 쓴다면 push로 upLoadFileList에 추가한다.

그리고 nodelibrary의 uploadFiles를 호출하면 된다.

const uploadFiles = (e) => {
  if(e.target.files.length === 0) return;

  let objFileList = {}; /* 중복 체크를 위한 object */
  for(let item of fileList) objFileList[item] = true;

  let uploadFileList = [];
  for(let i = 0; i < e.target.files.length; i++) {
    let fileName = e.target.files[i].name;

    if(objFileList[fileName] === true) {
      let answer = window.confirm(`${fileName}이(가) 이미 있습니다.\n바꾸시겠습니까?`);
      if(answer === false) continue;
    }
    
    uploadFileList.push(e.target.files[i]);
  }

  if(uploadFileList.length === 0) return;
  
  let path = `${mnode.PATH}/${pathInfo.version}/${pathInfo.country}`;
  mnode.uploadFiles(path, uploadFileList, function () {
    mnode.getFileList(path, "csv", setFileList);
  });

  return;
}

 

파일을 한꺼번에 업로드해보고, 중복된 이름의 파일도 업로드해서 alert가 제대로 나오는지 확인해보자.

 

최종 코드는 아래와 같다.

 

Node js

// fileUpload.js
const express = require("express");
const router = express.Router();
const multer = require("multer"); // npm install multer

router.post("/", (req, res) => {
  let path = req.query.path;
  const storage = multer.diskStorage({
    destination: (req, file, callback) => {
        callback(null, `${path}`);	//업로드 파일의 저장 위치를 설정
    },
    filename: (req, file, callback) => {
        callback(null, `${file.originalname}`);	// 파일이 저장될 때 이름 설정
    },
  });
  
  const limits = {
    files: 50,
    fileSize: 1024 * 1024 * 1024, //1G
  }

  const upload = multer({ storage, limits }).any();

  const reqFiles = [];

  upload(req, res, (err) => {
    if (err) {
      return res.json({ success: false, err });
    }

    console.log(req.files);
    for(let i = 0; i < req.files.length; i++) {
      reqFiles.push(req.files[i].fileName);
    }

    return res.json({
      success: true,
      //url: res.req.file.path,
      fileName: reqFiles,
    });
  });

  return;
 });
 
module.exports = router;

 

//server.js
const express = require('express');
const app = express();
const nodetest = require('./routes/nodetest');
const getFileFolderList = require('./routes/getFileFolderList');
const getFile = require('./routes/getFile');
const fileSave = require('./routes/fileSave');
const fileDelete = require('./routes/fileDelete');
const fileUpload = require('./routes/fileUpload');

const cors = require('cors');
app.use(cors()); //npm install cors --save

app.use('/nodetest', nodetest);
app.use('/getFileFolderList', getFileFolderList);
app.use('/getFile', getFile);
app.use('/fileSave', fileSave);
app.use('/fileDelete', fileDelete);
app.use('/fileUpload', fileUpload);

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

 

React

//nodelibrary.js
import axios from "axios";

export const MY_SERVER = `http://192.168.55.120:3002`;
export const PATH = `C:\\Users\\username\\Downloads\\TESTFILES`;

export const getFileFolderList = (path, fileExtension) => {
    fetch(`${MY_SERVER}/getFileFolderList?path=${path}&fileExtension=${fileExtension}`)
    .then((response) => response.json())
    .then((data) => console.log(data));
}

export const getFolderList = (setState, path) => {
    fetch(`${MY_SERVER}/getFileFolderList?path=${path}`)
    .then((response) => response.json())
    .then((data) => setState(data.folderList.map(list => list.name)));
}

export const getFileList = (path, fileExtension, setState) => {
    fetch(`${MY_SERVER}/getFileFolderList?path=${path}&fileExtension=${fileExtension}`)
    .then((response) => response.json())
    .then((data) => setState(data.fileList.map(list => list.name)));
}

export const getFile = (path) => { /* this is for just test */
    fetch(`${MY_SERVER}/getFile?path=${path}`)
    .then((response) => response.json())
    .then((data) => console.log(data));
}

export function deleteFiles(fileName, callback) {
  if(fileName === undefined) return;
  
  fetch(`${MY_SERVER}/fileDelete?fileName=${fileName}`)
  .then((res) => res.json())
  .then((data) => {
    (data.result === "success") ? alert("파일 삭제 성공") :  alert("파일 삭제 실패 : 파일이 없습니다. ");
    if(callback) callback();
  })
  .catch((error) => console.log(error));
}

export function uploadFiles(path, fileList, callback) {
  let formData = new FormData();
  
  const config = {
    header: { "content-type": "multipart/form-data" },
  };

  for(let i = 0; i < fileList.length; i++) formData.append("file", fileList[i]);

  axios
    .post(`${MY_SERVER}/fileUpload?path=${path}`, formData, config)
    .then((response) => {
      console.log(response);
      if (response.data.success) {
        console.log(response.data);
        alert(`${response.data.fileName.length}개 파일 업로드 성공`)
        if(callback) callback();
      } else {
        alert("업로드에 실패했습니다.");
      }
    });
}

 

/* eslint-disable react-hooks/exhaustive-deps */
//MyTable.js
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import * as lib from "./library.js";
import * as mnode from "./nodelibrary";

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

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,
  pathInfo,
  fileList,
  setFileList,
}) => {
  const [displayIndex, setDisplayIndex] = useState("");
  const [displayCell, setDisplayCell] = useState("");
  const [value, setValue] = useState("");

  const saveFile = () => {
    for (let name of fileList) {
      if (name === value) {
        let answer = window.confirm(
          `${name}이(가) 이미 있습니다.\n바꾸시겠습니까?`
        );
        if (answer === false) return;
      }
    }

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

    let filePath = `${mnode.PATH}/${pathInfo.version}/${pathInfo.country}`;
    let fileName = `${filePath}/${value}`;

    const config = {
      header: { "content-type": "application/json" },
    };

    axios
      .post(
        `${mnode.MY_SERVER}/fileSave?fileName=${fileName}`,
        { file: realTable },
        config
      )
      .then((response) => {
        if (response.status === 200)
          mnode.getFileList(filePath, "csv", setFileList);
      });

    alert(
      `${value} 파일이 ${pathInfo.version}/${pathInfo.country}에 저장되었습니다.`
    );

    return;
  };

  const deleteFile = () => {
    if (pathInfo.version === "" || pathInfo.country === "" || pathInfo.file === "") {
      alert("version / country / file을 모두 선택하세요.");
      return;
    }

    let filePath = `${mnode.PATH}/${pathInfo.version}/${pathInfo.country}`;
    let fileName = `${filePath}/${pathInfo.file}`;

    let answer = window.confirm(`${fileName}를 정말 삭제하시겠습니까?`);
    if (answer === false) return;

    mnode.deleteFiles(fileName, function () {
      mnode.getFileList(filePath, "csv", setFileList);
    });
  };

  const uploadFiles = (e) => {
    if(e.target.files.length === 0) return;

    let objFileList = {}; /* 중복 체크를 위한 object */
    for(let item of fileList) objFileList[item] = true;

    let uploadFileList = [];
    for(let i = 0; i < e.target.files.length; i++) {
      let fileName = e.target.files[i].name;

      if(objFileList[fileName] === true) {
        let answer = window.confirm(`${fileName}이(가) 이미 있습니다.\n바꾸시겠습니까?`);
        if(answer === false) continue;
      }
      
      uploadFileList.push(e.target.files[i]);
    }

    if(uploadFileList.length === 0) return;
    
    let path = `${mnode.PATH}/${pathInfo.version}/${pathInfo.country}`;
    mnode.uploadFiles(path, uploadFileList, function () {
      mnode.getFileList(path, "csv", setFileList);
    });

    return;
  }

  const onClickUpload = () => {
    if(pathInfo.version === "" || pathInfo.country === "") {
      alert("version / country를 모두 선택하세요.");
      return;
    }

    document.getElementById("hidden-upload").click();
  }

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

    if (myTable !== undefined) myTable.destroy();

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

  useEffect(() => {
    setValue(pathInfo.file);
  }, [pathInfo.file]);

  return (
    <div>
      {fileUploadFlag && (
        <div>
          <button onClick={csvDownLoad}>DOWNLOAD</button>
          <button onClick={saveFile}>SAVE</button>
          <button onClick={deleteFile}>DELETE</button>
          <button onClick={onClickUpload}>UPLOAD</button>
          <AutoSizeInput
            placeholder="파일 이름 입력"
            value={value}
            onChange={(e) => setValue(e.target.value)}
          />
          <div>
            <span>{displayIndex}</span>
            <input value={displayCell} onChange={setValueCell} />
          </div>
          <input
            type="file"
            id="hidden-upload"
            style={{ visibility: "hidden" }}
            accept=".csv"
            multiple
            onChange={(e) => uploadFiles(e)}
          />
          <div id="hot-app"></div>
        </div>
      )}
    </div>
  );
};

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

export default connect(mapStateToProps)(MyTable);

이전 - (13) shelljs를 이용하여 server의 파일 삭제하기

 

반응형

댓글