이제 첫 리포트를 생성해보자~!!
작업일지 리포트는 테이블형태(엑셀)로 작성하고 엑셀과 csv 파일로 내보내고 불러오게 가능하도록 구상을 했다.
마침 테이블 형태 관련해서 좋은 글과 좋은 라이브러리를 찾게 되었다.
https://handsontable.com/docs/react-data-grid/
https://bloodstrawberry.tistory.com/272
handsontable
handsontable 이란 라이브러리는 잘 알려진 스프레드시트의 모양과 느낌을 애플리케이션에 제공하는 자바스크립트 데이터 그리드 구성 요소 라고 공식문서에서 설명한다
npm install handsontable @handsontable/react
으로 설치를 진행하고
index.css에 handsontable 라이브러리 css 를 불러와저야 한다고 한다
import 'handsontable/dist/handsontable.full.min.css';
그리고 사용할 컴포넌트에서
import { HotTable } from '@handsontable/react';
<HotTable
data={[
['', 'Tesla', 'Volvo', 'Toyota', 'Ford'],
['2019', 10, 11, 12, 13],
['2020', 20, 11, 14, 13],
['2021', 30, 15, 12, 13]
]}
rowHeaders={true}
colHeaders={true}
height="auto"
licenseKey="non-commercial-and-evaluation" // for non-commercial use only
/>
다양한 옵션들(이번 프로젝트에서 사용한 옵션들)
- id : 테이블을 불러오기 위해 id를 지정해주어야 한다. hot 으로 고정
ex) id="hot" - data: 테이블에 표시할 데이터를 넣어야 한다.
ex) data={data && data} || 데이터를 넣어주지 않으면 기본 테이블이 나온다 - colHeaders: 기본 열 머리글(A, B, C)을 사용하거나 배열 또는 함수에서 제공하는 사용자 지정 값으로 설정
ex) colHeaders={colHeaders} || colHeaders?: boolean | string[] | ((index: number) => string); - rowHeaders: 기본 행 헤더(1, 2, 3)를 사용하거나 배열 또는 함수에서 제공하는 사용자 지정 값으로 설정합니다.
ex) rowHeaders={true} || rowHeaders?: boolean | string[] | ((index: number) => string); - manualColumnMove: 컬럼을 이동 설정
ex) manualColumnMove={true} - fixedColumnsStart: 지정된 열의 위치를 지정하면 스크롤할 때 해당 열이 계속 표시
ex) fixedColumnsStart={window.innerWidth > 1024 ? 1 : 0} - htCenter / htMiddle: 셀 텍스트 정렬 관련 설정 (중앙정렬 / 세로 중앙정렬)
ex) className="htCenter htMiddle z-0" colWidths={colWidths()} - rowHeights: 셀 높이 지정
ex) rowHeights={`${(window.innerHeight - 200) / 10}`} - readOnly: true 면 편집이 불가능한 모드로 설정
ex) readOnly={readOnly ? readOnly : false} - contextMenu: 컨텍스트 메뉴를 생성. 그리드의 다른 기능 중 원하는 위치에 새 행이나 열을 만들 수 있다.
ex) contextMenu={true} - manualColumnResize: 컬럼 너비를 수정하도록 설정
ex) manualColumnResize={true} - dropdownMenu: 드롭다운 메뉴 설정
ex) dropdownMenu={true} - columnSorting: 컬럼당 정렬기능을 설정
columnSorting={true} - afterColumnSort : 열을 정렬한 후 ColumnSorting 및 MultiColumnSorting 플러그인에서 발생하는 훅, 함수를 전달
afterColumnSort={exclude}
handsontable에서 데이터 추출해서 파이어스토어로 넘기기
파이어베이스 데이터 저장 API 짜기
파이어스토어에서 데이터를 추가하는 방식 setDoc() vs addDoc()
setDoc()
단일 문서 생성 및 덮어쓰기는 setDoc()메서드를 사용한다. 문서가 없으면 생성하고 문서가 있으면 새로 제공한 데이터로 내용을 덮어쓴다. 단 setDoc()을 사용하게 되면 문서의 ID를 지정을 해줘야 한다
이번 프로젝트에서 사용한 문서 ID 생성 라이브러리
reportId는 id를 생성해주는 uuid.js 라이브러리를 사용
https://www.npmjs.com/package/uuid
타임스탬프를 기준으로 uuid를 생성해주는 v1을 사용하였다.
import { v1 as uuidv1 } from "uuid";
export async function addReport(data) {
const reportId = uuidv1();
await setDoc(doc(db, "reports", reportId), data);
}
addDoc()
문서에 유의미한 ID를 두지 않고 Cloud Firestore에서 자동으로 ID를 생성하도록 할때 사용하는 addDoc()을 사용한다
이번 프로젝트에서는
문서 정보에 reportId 값을 따로 저장해주고 싶어 setDoc() 을 사용하였다.
또한, 작성과 동시에 작성된 페이지로 이동해주기 위해 reportId 를 차가로 반환해주었다
firestore.js
export async function addReport(headers, data, user, title) {
const reportId = uuidv1();
await setDoc(doc(db, "reports", reportId), {
headers: headers,
data: data,
userId: user.uid,
createdAt: serverTimestamp(),
title: title,
reportId: reportId,
fix: false,
writer: user.displayName,
});
return reportId;
}
사용예시
await addReport(headers, data, user, title).then((reportId) => {
navigate(`/reports/${reportId}`);
});
handontalbe 에서 데이터 추출
https://handsontable.com/docs/react-data-grid/saving-data/
writePage.jsx
import React, { useRef } from "react";
import { registerAllModules } from "handsontable/registry";
//.
//.
//.
registerAllModules();
const hotRef = useRef(null);
//.
//.
//.
const saveClickCallback = async (e) => {
e.preventDefault();
hot = hotRef.current.hotInstance;
let data = JSON.stringify(hot.getData());
await addReport(data).then(() => {
navigate("/");
});
};
return (
<div className="w-full">
<form onSubmit={(...arg) => saveClickCallback(...arg)}>
<HotTableOption colHeaders={true} data={data} hotRef={hotRef} />
</form>
</div>
);
데이터는 getData()함수로 배열 형태로 받아오고
JSON.stringify() 를 통해 JSON 문자열로 변환 해주어 기존에 만든 addReport()에 전달해준다
firebase 콘솔에서 확인된 JSON 데이터
HotTable은 총 3곳이상의 페이지에서 사용될 예정이라 컴포넌트화 해서 빼두었다
import { HotTable } from "@handsontable/react";
import React from "react";
export default function HotTableOption({tableData, data,hotRef,colHeaders, readOnly}) {
// 첫번째 열은 sort 되지 않도록 하는 함수
const exclude = () => {
const handsontableInstance = hotRef.current.hotInstance;
const lastRowIndex = handsontableInstance.countRows() - 1;
handsontableInstance.rowIndexMapper.moveIndexes(
handsontableInstance.toVisualRow(0),
0
);
handsontableInstance.rowIndexMapper.moveIndexes(
handsontableInstance.toVisualRow(lastRowIndex),
lastRowIndex
);
};
// colWidth를 반환하는 함수
const colWidths = () => {
let colWidths = [];
if (tableData) {
if (window.innerWidth < 1024) {
for (let i = 0; i < JSON.parse(tableData.data)[0].length; i++) {
colWidths.push(100);
}
return colWidths;
}
for (let i = 0; i < JSON.parse(tableData.data)[0].length; i++) {
colWidths.push(
window.innerWidth / JSON.parse(tableData.data)[0].length
);
}
return colWidths;
} else {
return (colWidths =
window.innerWidth > 1024
? (window.innerWidth - 300) / 5
: window.innerWidth / 5);
}
};
return (
<HotTable
id="hot"
data={data && data}
colHeaders={colHeaders}
rowHeaders={true}
manualColumnMove={true}
fixedColumnsStart={window.innerWidth > 1024 ? 1 : 0}
className="htCenter htMiddle z-0"
colWidths={colWidths()}
rowHeights={`${(window.innerHeight - 200) / 10}`}
licenseKey="non-commercial-and-evaluation"
readOnly={readOnly ? readOnly : false}
ref={hotRef}
contextMenu={true}
manualColumnResize={true}
dropdownMenu={true}
columnSorting={true}
width={"100%"}
afterColumnSort={exclude}
// for non-commercial use only
/>
);
}