본문 바로가기
개발/Node JS

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

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

프로젝트 전체 링크

 

이전 - (10) csv 파일 handsontable로 연동하기

현재 - (11) AutoSizeInput에 파일 이름 연동하기

다음 - (12) axios post로 파일 저장하기

 

깃허브에서 코드 확인하기


이제 편집한 csv를 server에 저장해보자.

server에 저장하기 전에 파일 이름을 불러오고, 파일 이름도 수정할 수 있는 input을 먼저 구현해보자.

 

저장할 file의 이름이 있어야, 해당되는 파일이 서버에 없는 경우 그대로 저장하고, 

서버에 있다면, 덮어 씌울 건지 확인할 수 있다.

 

input은 파일 이름에 따라 size가 알맞게 수정되도록 하자.

AutoSizeInput.js를 만들고 아래의 코드를 복사하자.

//AutoSizeInput.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';

const sizerStyle = {
	position: 'absolute',
	top: 0,
	left: 0,
	visibility: 'hidden',
	height: 0,
	overflow: 'scroll',
	whiteSpace: 'pre',
};

const INPUT_PROPS_BLACKLIST = [
	'extraWidth',
	'injectStyles',
	'inputClassName',
	'inputRef',
	'inputStyle',
	'minWidth',
	'onAutosize',
	'placeholderIsMinWidth',
];

const cleanInputProps = (inputProps) => {
	INPUT_PROPS_BLACKLIST.forEach(field => delete inputProps[field]);
	return inputProps;
};

const copyStyles = (styles, node) => {
	node.style.fontSize = styles.fontSize;
	node.style.fontFamily = styles.fontFamily;
	node.style.fontWeight = styles.fontWeight;
	node.style.fontStyle = styles.fontStyle;
	node.style.letterSpacing = styles.letterSpacing;
	node.style.textTransform = styles.textTransform;
};

const isIE = (typeof window !== 'undefined' && window.navigator) ? /MSIE |Trident\/|Edge\//.test(window.navigator.userAgent) : false;

const generateId = () => {
	// we only need an auto-generated ID for stylesheet injection, which is only
	// used for IE. so if the browser is not IE, this should return undefined.
	return isIE ? '_' + Math.random().toString(36).substr(2, 12) : undefined;
};

class AutoSizeInput extends Component {
	static getDerivedStateFromProps (props, state) {
		const { id } = props;
		return id !== state.prevId ? { inputId: id || generateId(), prevId: id } : null;
	}
	constructor (props) {
		super(props);
		this.state = {
			inputWidth: props.minWidth,
			inputId: props.id || generateId(),
			prevId: props.id,
		};
	}
	componentDidMount () {
		this.mounted = true;
		this.copyInputStyles();
		this.updateInputWidth();
	}
	componentDidUpdate (prevProps, prevState) {
		if (prevState.inputWidth !== this.state.inputWidth) {
			if (typeof this.props.onAutosize === 'function') {
				this.props.onAutosize(this.state.inputWidth);
			}
		}
		this.updateInputWidth();
	}
	componentWillUnmount () {
		this.mounted = false;
	}
	inputRef = (el) => {
		this.input = el;
		if (typeof this.props.inputRef === 'function') {
			this.props.inputRef(el);
		}
	};
	placeHolderSizerRef = (el) => {
		this.placeHolderSizer = el;
	};
	sizerRef = (el) => {
		this.sizer = el;
	};
	copyInputStyles () {
		if (!this.mounted || !window.getComputedStyle) {
			return;
		}
		const inputStyles = this.input && window.getComputedStyle(this.input);
		if (!inputStyles) {
			return;
		}
		copyStyles(inputStyles, this.sizer);
		if (this.placeHolderSizer) {
			copyStyles(inputStyles, this.placeHolderSizer);
		}
	}
	updateInputWidth () {
		if (!this.mounted || !this.sizer || typeof this.sizer.scrollWidth === 'undefined') {
			return;
		}
		let newInputWidth;
		if (this.props.placeholder && (!this.props.value || (this.props.value && this.props.placeholderIsMinWidth))) {
			newInputWidth = Math.max(this.sizer.scrollWidth, this.placeHolderSizer.scrollWidth) + 2;
		} else {
			newInputWidth = this.sizer.scrollWidth + 2;
		}
		// add extraWidth to the detected width. for number types, this defaults to 16 to allow for the stepper UI
		const extraWidth = (this.props.type === 'number' && this.props.extraWidth === undefined)
			? 16 : parseInt(this.props.extraWidth) || 0;
		newInputWidth += extraWidth;
		if (newInputWidth < this.props.minWidth) {
			newInputWidth = this.props.minWidth;
		}
		if (newInputWidth !== this.state.inputWidth) {
			this.setState({
				inputWidth: newInputWidth,
			});
		}
	}
	getInput () {
		return this.input;
	}
	focus () {
		this.input.focus();
	}
	blur () {
		this.input.blur();
	}
	select () {
		this.input.select();
	}
	renderStyles () {
		// this method injects styles to hide IE's clear indicator, which messes
		// with input size detection. the stylesheet is only injected when the
		// browser is IE, and can also be disabled by the `injectStyles` prop.
		const { injectStyles } = this.props;
		return isIE && injectStyles ? (
			<style dangerouslySetInnerHTML={{
				__html: `input#${this.state.inputId}::-ms-clear {display: none;}`,
			}} />
		) : null;
	}
	render () {
		const sizerValue = [this.props.defaultValue, this.props.value, ''].reduce((previousValue, currentValue) => {
			if (previousValue !== null && previousValue !== undefined) {
				return previousValue;
			}
			return currentValue;
		});

		const wrapperStyle = { ...this.props.style };
		if (!wrapperStyle.display) wrapperStyle.display = 'inline-block';

		const inputStyle = {
			boxSizing: 'content-box',
			width: `${this.state.inputWidth}px`,
			...this.props.inputStyle,
		};

		const { ...inputProps } = this.props;
		cleanInputProps(inputProps);
		inputProps.className = this.props.inputClassName;
		inputProps.id = this.state.inputId;
		inputProps.style = inputStyle;

		return (
			<div className={this.props.className} style={wrapperStyle}>
				{this.renderStyles()}
				<input {...inputProps} ref={this.inputRef} />
				<div ref={this.sizerRef} style={sizerStyle}>{sizerValue}</div>
				{this.props.placeholder
					? <div ref={this.placeHolderSizerRef} style={sizerStyle}>{this.props.placeholder}</div>
					: null
				}
			</div>
		);
	}
}

AutoSizeInput.propTypes = {
	className: PropTypes.string,               // className for the outer element
	defaultValue: PropTypes.any,               // default field value
	extraWidth: PropTypes.oneOfType([          // additional width for input element
		PropTypes.number,
		PropTypes.string,
	]),
	id: PropTypes.string,                      // id to use for the input, can be set for consistent snapshots
	injectStyles: PropTypes.bool,              // inject the custom stylesheet to hide clear UI, defaults to true
	inputClassName: PropTypes.string,          // className for the input element
	inputRef: PropTypes.func,                  // ref callback for the input element
	inputStyle: PropTypes.object,              // css styles for the input element
	minWidth: PropTypes.oneOfType([            // minimum width for input element
		PropTypes.number,
		PropTypes.string,
	]),
	onAutosize: PropTypes.func,                // onAutosize handler: function(newWidth) {}
	onChange: PropTypes.func,                  // onChange handler: function(event) {}
	placeholder: PropTypes.string,             // placeholder text
	placeholderIsMinWidth: PropTypes.bool,     // don't collapse size to less than the placeholder
	style: PropTypes.object,                   // css styles for the outer element
	value: PropTypes.any,                      // field value
};
AutoSizeInput.defaultProps = {
	minWidth: 1,
	injectStyles: true,
};

export default AutoSizeInput;

위 코드는 링크를 참고하였다.

 

MyTable.js에 AutoSizeInput component를 추가하자.

//MyTable.js

...

const [value, setValue] = useState("");

...

<AutoSizeInput
    placeholder="파일 이름 입력"
    value={value}
    onChange={(e) => setValue(e.target.value)}
/>

 

그러면 아래처럼 한글/영어에 상관없이 알맞게 input의 size가 조절된다.


이제 node의 파일을 불러오면 FileName이 AutoSizeInput에 들어가도록 변경하자.

 

file의 이름은 App.js의 file에서 useState를 이용하고 있으므로, file을 MyTable에 넘긴다.

//App.js

...

<MyTable csvFile={csvObject} file={file}/>

 

file이 변경될 때마다, setValue를 호출해서 fileName을 변경해주면 autoSizeInput에 반영된다.

//MyTable.js

const MyTable = ({ csvFile, fileUploadFlag, file }) => {

...

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

 

최종 코드는 아래와 같다.

 

React

//AutoSizeInput.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';

const sizerStyle = {
	position: 'absolute',
	top: 0,
	left: 0,
	visibility: 'hidden',
	height: 0,
	overflow: 'scroll',
	whiteSpace: 'pre',
};

const INPUT_PROPS_BLACKLIST = [
	'extraWidth',
	'injectStyles',
	'inputClassName',
	'inputRef',
	'inputStyle',
	'minWidth',
	'onAutosize',
	'placeholderIsMinWidth',
];

const cleanInputProps = (inputProps) => {
	INPUT_PROPS_BLACKLIST.forEach(field => delete inputProps[field]);
	return inputProps;
};

const copyStyles = (styles, node) => {
	node.style.fontSize = styles.fontSize;
	node.style.fontFamily = styles.fontFamily;
	node.style.fontWeight = styles.fontWeight;
	node.style.fontStyle = styles.fontStyle;
	node.style.letterSpacing = styles.letterSpacing;
	node.style.textTransform = styles.textTransform;
};

const isIE = (typeof window !== 'undefined' && window.navigator) ? /MSIE |Trident\/|Edge\//.test(window.navigator.userAgent) : false;

const generateId = () => {
	// we only need an auto-generated ID for stylesheet injection, which is only
	// used for IE. so if the browser is not IE, this should return undefined.
	return isIE ? '_' + Math.random().toString(36).substr(2, 12) : undefined;
};

class AutoSizeInput extends Component {
	static getDerivedStateFromProps (props, state) {
		const { id } = props;
		return id !== state.prevId ? { inputId: id || generateId(), prevId: id } : null;
	}
	constructor (props) {
		super(props);
		this.state = {
			inputWidth: props.minWidth,
			inputId: props.id || generateId(),
			prevId: props.id,
		};
	}
	componentDidMount () {
		this.mounted = true;
		this.copyInputStyles();
		this.updateInputWidth();
	}
	componentDidUpdate (prevProps, prevState) {
		if (prevState.inputWidth !== this.state.inputWidth) {
			if (typeof this.props.onAutosize === 'function') {
				this.props.onAutosize(this.state.inputWidth);
			}
		}
		this.updateInputWidth();
	}
	componentWillUnmount () {
		this.mounted = false;
	}
	inputRef = (el) => {
		this.input = el;
		if (typeof this.props.inputRef === 'function') {
			this.props.inputRef(el);
		}
	};
	placeHolderSizerRef = (el) => {
		this.placeHolderSizer = el;
	};
	sizerRef = (el) => {
		this.sizer = el;
	};
	copyInputStyles () {
		if (!this.mounted || !window.getComputedStyle) {
			return;
		}
		const inputStyles = this.input && window.getComputedStyle(this.input);
		if (!inputStyles) {
			return;
		}
		copyStyles(inputStyles, this.sizer);
		if (this.placeHolderSizer) {
			copyStyles(inputStyles, this.placeHolderSizer);
		}
	}
	updateInputWidth () {
		if (!this.mounted || !this.sizer || typeof this.sizer.scrollWidth === 'undefined') {
			return;
		}
		let newInputWidth;
		if (this.props.placeholder && (!this.props.value || (this.props.value && this.props.placeholderIsMinWidth))) {
			newInputWidth = Math.max(this.sizer.scrollWidth, this.placeHolderSizer.scrollWidth) + 2;
		} else {
			newInputWidth = this.sizer.scrollWidth + 2;
		}
		// add extraWidth to the detected width. for number types, this defaults to 16 to allow for the stepper UI
		const extraWidth = (this.props.type === 'number' && this.props.extraWidth === undefined)
			? 16 : parseInt(this.props.extraWidth) || 0;
		newInputWidth += extraWidth;
		if (newInputWidth < this.props.minWidth) {
			newInputWidth = this.props.minWidth;
		}
		if (newInputWidth !== this.state.inputWidth) {
			this.setState({
				inputWidth: newInputWidth,
			});
		}
	}
	getInput () {
		return this.input;
	}
	focus () {
		this.input.focus();
	}
	blur () {
		this.input.blur();
	}
	select () {
		this.input.select();
	}
	renderStyles () {
		// this method injects styles to hide IE's clear indicator, which messes
		// with input size detection. the stylesheet is only injected when the
		// browser is IE, and can also be disabled by the `injectStyles` prop.
		const { injectStyles } = this.props;
		return isIE && injectStyles ? (
			<style dangerouslySetInnerHTML={{
				__html: `input#${this.state.inputId}::-ms-clear {display: none;}`,
			}} />
		) : null;
	}
	render () {
		const sizerValue = [this.props.defaultValue, this.props.value, ''].reduce((previousValue, currentValue) => {
			if (previousValue !== null && previousValue !== undefined) {
				return previousValue;
			}
			return currentValue;
		});

		const wrapperStyle = { ...this.props.style };
		if (!wrapperStyle.display) wrapperStyle.display = 'inline-block';

		const inputStyle = {
			boxSizing: 'content-box',
			width: `${this.state.inputWidth}px`,
			...this.props.inputStyle,
		};

		const { ...inputProps } = this.props;
		cleanInputProps(inputProps);
		inputProps.className = this.props.inputClassName;
		inputProps.id = this.state.inputId;
		inputProps.style = inputStyle;

		return (
			<div className={this.props.className} style={wrapperStyle}>
				{this.renderStyles()}
				<input {...inputProps} ref={this.inputRef} />
				<div ref={this.sizerRef} style={sizerStyle}>{sizerValue}</div>
				{this.props.placeholder
					? <div ref={this.placeHolderSizerRef} style={sizerStyle}>{this.props.placeholder}</div>
					: null
				}
			</div>
		);
	}
}

AutoSizeInput.propTypes = {
	className: PropTypes.string,               // className for the outer element
	defaultValue: PropTypes.any,               // default field value
	extraWidth: PropTypes.oneOfType([          // additional width for input element
		PropTypes.number,
		PropTypes.string,
	]),
	id: PropTypes.string,                      // id to use for the input, can be set for consistent snapshots
	injectStyles: PropTypes.bool,              // inject the custom stylesheet to hide clear UI, defaults to true
	inputClassName: PropTypes.string,          // className for the input element
	inputRef: PropTypes.func,                  // ref callback for the input element
	inputStyle: PropTypes.object,              // css styles for the input element
	minWidth: PropTypes.oneOfType([            // minimum width for input element
		PropTypes.number,
		PropTypes.string,
	]),
	onAutosize: PropTypes.func,                // onAutosize handler: function(newWidth) {}
	onChange: PropTypes.func,                  // onChange handler: function(event) {}
	placeholder: PropTypes.string,             // placeholder text
	placeholderIsMinWidth: PropTypes.bool,     // don't collapse size to less than the placeholder
	style: PropTypes.object,                   // css styles for the outer element
	value: PropTypes.any,                      // field value
};
AutoSizeInput.defaultProps = {
	minWidth: 1,
	injectStyles: true,
};

export default AutoSizeInput;

 

//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";
import AutoSizeInput from "./AutoSizeInput";

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

  return (
    <div>
      {fileUploadFlag && (
        <div>
          <button onClick={csvDownLoad}>DOWNLOAD</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);

 

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

import FileUpload from "./components/FileUpload";
import MyFileList from "./components/MyFileList";
import MyTable from "./components/MyTable";
import MyToggles from "./components/MyToggles";
import * as mnode from "./components/nodelibrary";

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

const nodeTest = () => {
  mnode.getFileFolderList(mnode.PATH, "csv");
  return;
}

const App = () => {
  const [csvObject, setCsvObject] = useState(csvObjectDefault);
  const [version, setVersion] = useState("");
  const [country, setCountry] = useState("");
  const [file, setFile] = useState("");
  const [fileList, setFileList] = useState([]);

  const getFileList = () => {
    if(version === "" || country === "") return;

    let path = `${mnode.PATH}/${version}/${country}`;
    mnode.getFileList(path, "csv", setFileList);
  }

  useEffect(getFileList, [version, country]);

  return (
    <div>
      <MyToggles
        version={version}
        setVersion={setVersion}
        country={country}
        setCountry={setCountry}
      />
      
      <hr style={{ borderColor: "grey" }} />
      
      <MyFileList fileList={fileList} setFile={setFile}/>
      
      <button onClick={nodeTest}>서버 연결</button>
      <button onClick={() => console.log(csvObject)}>print csv</button>
      <div className="App">
        <FileUpload setCsvObject={setCsvObject} pathInfo={{ version, country, file }} />
        <MyTable csvFile={csvObject} file={file}/>
      </div>
    </div>
  );
};

export default App;

이전 - (10) csv 파일 handsontable로 연동하기

다음 - (12) axios post로 파일 저장하기

반응형

댓글