본문 바로가기
개발/Node JS

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

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

프로젝트 전체 링크

 

이전 - (12) axios post로 파일 저장하기

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

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

 

깃허브에서 코드 확인하기


이제 DELETE 버튼을 누르면 server의 파일이 삭제되도록 해보자.

 

파일의 삭제는 Git Bash에서 rm 명령을 이용하여 삭제할 수 있으므로,

node js에서 rm 명령을 내려서 파일을 지우자.

외부 명령어를 실행해주는 모듈인 shelljs를 설치한다.

npm install shelljs

 

server에 fileDelete.js를 만든다.

shell.exec을 이용하여 명령을 내리면 되고, fetch를 이용해 지워야할 file의 full path를 전달한다.

exec.code로 성공 여부를 알 수 있다.

//fileDelete.js
const slib = require("./serverlib");
const express = require("express");
const router = express.Router();
const shell = require("shelljs"); // npm install shelljs --save

router.get("/", (req, res) => {
  let fileName = slib.changeBlankFileNameWindow(req.query.fileName);

  shell.cd("~");

  let result;
  if (shell.exec(`rm ${fileName}`).code !== 0) {
    result = "failed";
  } else {
    result = "success";
  }

  res.send({ result });

  return;
});

module.exports = router;

 

이때, file의 이름에 공백이 들어갈 경우, file이름이 ''로 씌어줘야한다.

YOUR_WINDOW_PATH/TESTFILES/2020/China (master)
$ rm 'delete test.csv'

 

따라서 serverlib.js에 file이름의 공백에 대한 처리를 만드는 함수를 만든다.

//serverlib.js

...

/* window */
exports.changeBlankFileNameWindow = function(fileName) {
    let newFileName = fileName;

    if(newFileName.includes(" ")) newFileName = `'${fileName}'`;

    return newFileName;
}

/* linux */
exports.changeBlankFileNameLinux = function(fileName) {
    let newFileName = "";

    for(let i = 0; i < fileName.length; i++) {
        if(fileName[i] === " ") newFileName += '\\';
        newFileName += fileName[i];
    }

    return newFileName;
}

리눅스의 경우에는 공백앞에 \를 붙여서 구분하기 때문에 처리 방식이 다르다.

 

fileDelete를 추가하였으므로 server.js에도 추가한다.

//server.js

...

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

...

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


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

이제 React의 nodelibrary.js에 deleteFiles 함수를 만들자.

callback 함수는 file을 지운 후에 호출될 함수이다. 

파일을 삭제하면 fileList를 갱신할 필요가 있기 때문에, callback으로 넘겨준다.

그리고 result를 확인하여 alert를 보여주게 만든다.

//nodelibrary.js

...

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

 

MyTable.js에 deleteFile 함수를 만든다. version이나 country, file이 선택되지 않는다면 return 한다.

그리고 confirm을 이용해 정말 삭제할지 한번 물어보자.
fileList의 갱신은 saveFile에서 사용한 mnode.getFileList를 callback function으로 넘겨준다.

//MyTable.js

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

 

이제 button을 만들어서 event를 연결해주자.

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

 

파일을 임의로 만든 후, 삭제해보자. 공백이 들어간 파일명도 잘 삭제된다.

 

최종 코드는 아래와 같다.

//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 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.listen(3002, () => console.log('Node.js Server is running on port 3002...'));

 

//fileDelete.js
const slib = require("./serverlib");
const express = require("express");
const router = express.Router();
const shell = require("shelljs"); // npm install shelljs --save

router.get("/", (req, res) => {
  let fileName = slib.changeBlankFileNameWindow(req.query.fileName);

  shell.cd("~");

  let result;
  if (shell.exec(`rm ${fileName}`).code !== 0) {
    result = "failed";
  } else {
    result = "success";
  }

  res.send({ result });

  return;
});

module.exports = router;

 

//serverlib.js
exports.isFileExtension = function(fileName, extension) {
    if(extension === undefined) return true;

    let spt = fileName.split(".");
    let length = spt.length;
    
    if(spt[length - 1].toUpperCase() === extension.toUpperCase()) return true;

    return false;
};

/* window */
exports.changeBlankFileNameWindow = function(fileName) {
    let newFileName = fileName;

    if(newFileName.includes(" ")) newFileName = `'${fileName}'`;

    return newFileName;
}

/* linux */
exports.changeBlankFileNameLinux = function(fileName) {
    let newFileName = "";

    for(let i = 0; i < fileName.length; i++) {
        if(fileName[i] === " ") newFileName += '\\';
        newFileName += fileName[i];
    }

    return newFileName;
}

 

React

//nodelibrary.js
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));
}

 

/* 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 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>
          <AutoSizeInput
            placeholder="파일 이름 입력"
            value={value}
            onChange={(e) => setValue(e.target.value)}
          />
          <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);

이전 - (12) axios post로 파일 저장하기

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

반응형

댓글