레이아웃
엑셀 업로드나 다운로드 기능만 따로 사용하고 싶을 수 있기 때문에 enable{Uploader or Downlaoder}를 통해 한 가지 기능만 사용할 수 있게 했다.
업로드를 위해 전체 다운로드 요청을 보내는 함수, 데이터 type, 생성을 위한 함수 등을 prop으로 받아와서 사용했다.
export default function ExcelHandler({type, createParams, createParamsOrder, updateList, getFullListParams, restrictionMsg="", uploaderTitle=null, enableUploader = true, enableDownloader = true }) {
return (
<>
{enableUploader && <ExcelUploadButton
//...생략
/>}
{enableDownloader && <ExcelDownloadButton
// ...생략
/>}
</>
)
}
다운로드
전체 흐름
- 전체 데이터 조회
- 데이터를 엑셀에 넣고 싶은 속성만 골라서 파싱
- 엑셀 워크시트 생성
- 워크시트에 데이터 넣기
- 워크 북에 워크시트 넣고 워크북 다운로드
xlsx의 훌륭한 메소드들로 엑셀 파일에 데이터 넣기, 다운로드를 너무 쉽게 할 수 있었다.
const excelDownload = async () => {
// 전체 데이터 조회
const response = await getFullList[type](...getFullListParams)
let fullList = await response.result.data.elements
// 데이터 파싱
fullList = dtosToJson(type, fullList)
// 엑셀 워크 시트 생성
const ws = XLSX.utils.aoa_to_sheet([
[...sheetColumnNames[type], '등록일'],
])
// 워크시트에 데이터 넣기
fullList.forEach(item => {
XLSX.utils.sheet_add_aoa(
ws,
[
[
...dtoColumnNames[type].map(columnName => item[columnName]),
item["createdAt"]
]
],
{origin: -1}
)
})
// 워크시트 셀 크기 설정
ws['!cols'] = (function(){
let result = []
for(let i = 0; i < sheetColumnNames[type].length+1; i++) result.push({ wpx: 150})
return result
})()
// 워크북에 워크시트 넣기
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb,ws,'목록')
// 엑셀 파일 다운로드
XLSX.writeFile(wb, `${type}_LIST_${getNowDateTime()}.xlsx` )
}
데이터 파싱하기
하지만, 데이터들을 파싱하는 과정이 만만치 않았다.
데이터들을 파싱하는 함수의 초기 형태는 다음과 같았다.
export const dtosToJson = (type, dtos) => {
let json = dtos
switch (type) {
case "NAMED_ENTITY":
json.forEach(entity => {
// 시스템 객체인 경우
if (entity.key.substr(0,3) === "sys") {
entity.systemType = "SYSTEM"
}
// values 파싱
let values = entity.values.map(e => e.value).join(",")
entity.values = value
})
break
case "FAQ":
json.forEach(entity => {
entity.example = entity.sentences.map(sen => sen.text).join(", ")
entity.return = entity.faqNode[0].bubbleList[0].texts[0].message
})
break
default:
break
//..생략
}
return json
}
이런 식으로 타입별로 파싱하는 함수 동작을 따로 만들었다. 이렇다보니 코드 양이 너무 길고 중복되고 뭔가 억지로 하나로 합쳐놓은듯한 모양이었다.
그래서 추후에 리팩토링을 했다.
export const dtoColumnNames = {
NAMED_ENTITY: ["name" , "key", "dataType", "entityMin", "entityMax", "values.value", "systemType"],
//..생략
}
// dtoColumnName에 해당하는 dto 값 구하기
const getDtoVal = (columnNameParsed=[], dto={}, idx = 0) => {
console.log(dto,columnNameParsed[idx])
if(idx === columnNameParsed.length) return dto
else if(idx > columnNameParsed.length) return null
const key = columnNameParsed[idx]
if(Array.isArray(dto)) {
// 배열 전체를 원하는 경우
// ',' 로 join
return dto.map(el => getDtoVal(columnNameParsed, el[key], idx+1)).join(',')
} else if(typeof(dto) === "object") {
if(key.includes("[")) {
// val 배열의 특정 요소를 인덱스로 접근
const idxStart = key.indexOf("[")
const idxEnd = key.indexOf("]")
const targetIndex = parseInt(key.slice(idxStart+1, idxEnd))
const targetName = key.slice(0,idxStart)
return getDtoVal(columnNameParsed, dto[targetName][targetIndex], idx+1)
} else return getDtoVal(columnNameParsed, dto[key], idx+1)
} else {
if(idx === columnNameParsed.length -1) return dto
else return null
}
}
export const dtosToJson = (type, dtos) => {
let json = []
for(let dto of dtos) {
let object = {}
for(let columnName of [...dtoColumnNames[type], "createdAt"]) {
object[columnName] = getDtoVal(columnName.split("."), dto)
}
json.push(object)
}
return json
}
원래는 속성이름을 'values.value'로 주지 않고 'value'로 주고 얘를 찾는 로직을 dtosToJson
에 따로 넣어줬다.
그것들을 통일해서 찾는 속성이름을 "a.b.c[0]" 이런식으로 줘서 getDtoVal
함수로 찾도록 했다.
getDtoVal
은 재귀적으로 값을 찾는 함수다.
.split(".")
으로 key들을 배열에 담아놓는다.- key를 순회하면서 값을 찾는다.
1) 배열이면 다음 타겟에 대한 getDtoVal()을 재귀적으로 호출하고 그 리턴값들을 ","으로 join해서 최종 값을 가져온다.
2) 객체이면getDtoVal(obj[key])
역시 재귀적으로 호출해 값을 찾는다.
3) 1.에서 만든 배열의 마지막 원소이면서 string이나 number (나머지는 2가지 경우 뿐)이면 얘네를 리턴, 아니면 null을 리턴하게 했다.
사실 코드 양은 더 늘었다. 하지만 추후에 다른 데이터에 엑셀 다운로드 로직을 추가하면 할 수록 훨씬 편하고 간단하게 추가할 수 있고 상대적으로 코드 양도 훨씬 적게 된다.
배운 점
다운로드 로직을 만들면서 사실 다운로드 로직 자체는 진짜 쉬웠다. 하지만 여러 곳에서 공통으로 사용하는 모듈을 만들 때 고려해야할 것들이 엄청 많았다.
그래서 유지 보수를 많이 했는데, 하다보니 선언형 코딩의 장점을 알게 되었다. 동작을 쭉 기술한 함수들은 일일이 어디서 어떻게 값이 바뀌는지를 일일이 찍어보면서 디버깅을 해야해서 어렵다. 그러나 선언형으로 해놓으면 동작부가 의미별로 나눠져서 디버깅하기가 수월했다.
'프로젝트 회고 > 엑셀 다운로드, 업로드 공통 모듈 개발' 카테고리의 다른 글
4. 정리 - 엑셀 공통 모듈 (1) | 2022.08.27 |
---|---|
3. 개발 - 엑셀 업로드 (1) | 2022.08.27 |
1. 요구사항 정리와 라이브러리 선정하기 (0) | 2022.08.25 |