excel-sheet-to-json
Version:
A TypeScript/JavaScript library that converts Excel files to JSON with custom header mapping. Works in both Node.js and browser environments.
176 lines (145 loc) • 5.98 kB
text/typescript
import * as XLSX from 'xlsx';
// Buffer 타입은 Node.js 환경에서 사용되지만, TSDX 환경에서 전역적으로 접근 가능합니다.
// 브라우저에서는 ArrayBuffer가 사용됩니다.
export type FileData = Buffer | ArrayBuffer;
// 최종 출력 데이터 구조 정의
export interface ParseResult {
originHeaderNames: string[];
fields: string[];
header: { [key: string]: string };
body: any[];
}
export interface ParseOptions {
headerStartRowNumber: number; // 1-based
bodyStartRowNumber: number; // 1-based
headerNameToKey: { [excelHeaderName: string]: string }; // {'제품 명칭': 'productName'}
}
export function parse(
fileBuffer: any, // Buffer | ArrayBuffer (타입 추정)
options: ParseOptions
): ParseResult {
// 1. Buffer 읽기 및 워크북 생성
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
// 2. 빈 행 생략 문제 해결: range 옵션 사용
const sheetRef = worksheet['!ref'];
if (!sheetRef) {
return { originHeaderNames: [], fields: [], header: {}, body: [] }; // 💡 header 초기값 변경
}
const arrayData: any[][] = XLSX.utils.sheet_to_json(worksheet, {
header: 1,
range: sheetRef,
raw: true,
});
// 3. 인덱스 계산 (1-based -> 0-based)
const headerRowIndex = options.headerStartRowNumber - 1;
const bodyRowIndex = options.bodyStartRowNumber - 1;
// 4. 원본 헤더 추출
const rawHeaders: string[] = arrayData[headerRowIndex] || [];
const originHeaderNames: string[] = rawHeaders
.map(name => (name ? String(name).trim() : ''))
.filter(name => name !== '');
// 💡 5. fields 배열 및 header 객체 생성 (수정된 로직)
const fields: string[] = []; // 매핑 성공한 DB 키 목록 (순서 보존용)
const header: { [key: string]: string } = {}; // { DB 키: 원본 헤더 이름 }
originHeaderNames.forEach(originName => {
const dbKey = options.headerNameToKey[originName];
// 매핑 테이블에 존재하는 헤더만 처리
if (dbKey) {
fields.push(dbKey);
header[dbKey] = originName; // 💡 DB 키를 Key로, 원본 헤더 이름을 Value로 저장
}
});
// 6. 바디 데이터 (JSON 배열) 변환
const body = [];
for (let i = bodyRowIndex; i < arrayData.length; i++) {
const row = arrayData[i];
const jsonObject: { [key: string]: any } = {};
let isEmptyRow = true;
// 7. 각 열을 반복하며 JSON 객체 생성
// 💡 arrayData의 모든 열을 반복하는 것이 아니라, fields 배열의 순서대로 반복해야 합니다.
// 문제: 현재 fields 배열의 순서와 row[j]의 인덱스가 일치한다고 가정한 로직은
// 매핑되지 않은 헤더가 중간에 있을 경우 깨질 수 있습니다.
// 💡 해결책: 매핑 성공한 DB 키(fields) 순서대로 데이터를 찾아 할당합니다.
// 이 문제를 해결하기 위해, arrayData[headerRowIndex]에서 DB 키에 해당하는
// 원본 헤더의 인덱스를 찾아야 합니다.
let isRowDataValid = true;
for (const dbKey of fields) {
// 현재 DB 키에 매핑된 원본 헤더 이름
const originName = header[dbKey];
// 원본 헤더 이름이 arrayData[headerRowIndex]에서 몇 번째 인덱스(열)에 있는지 찾습니다.
// Array.prototype.indexOf를 사용하여 찾습니다.
const colIndex = rawHeaders.findIndex(
name => String(name).trim() === originName
);
if (colIndex !== -1) {
const cellValue = row[colIndex] || '';
if (
cellValue !== null &&
cellValue !== undefined &&
String(cellValue).trim() !== ''
) {
isEmptyRow = false;
}
jsonObject[dbKey] = cellValue;
} else {
// 이 필드는 헤더 행에 존재했지만, 어떤 이유로 찾을 수 없다면 오류로 간주할 수 있습니다.
// 여기서는 매핑을 건너뛰고 다음 필드로 넘어갑니다.
isRowDataValid = false;
}
}
// 8. 모든 값이 빈 문자열이거나 null인 행은 건너뜁니다.
// 💡 fields 배열을 기반으로 루프를 돌았으므로, row.length 대신 fields.length로 제어됩니다.
if (!isEmptyRow && isRowDataValid) {
body.push(jsonObject);
}
}
return {
originHeaderNames: originHeaderNames,
fields: fields,
header: header, // 💡 수정된 Key-Value 객체
body: body,
};
}
/**
* 브라우저 환경에서의 File 객체를 ArrayBuffer로 변환합니다.
* @param file - 브라우저 환경에서의 File 객체
* @returns ArrayBuffer로 변환된 파일 데이터
*/
export function fileToArrayBufferInClient(file: File): Promise<ArrayBuffer> {
// 파일이 유효한지 확인
if (!file || !(file instanceof File)) {
return Promise.reject(new Error('Input must be a valid File object.'));
}
return new Promise((resolve, reject) => {
const reader = new FileReader();
// 1. 성공적으로 읽었을 때 처리
reader.onload = event => {
// event.target.result는 readAsArrayBuffer 호출 시 ArrayBuffer 타입입니다.
const arrayBuffer = event.target?.result;
if (arrayBuffer instanceof ArrayBuffer) {
resolve(arrayBuffer);
} else {
reject(
new Error('File reading completed, but result is not ArrayBuffer.')
);
}
};
// 2. 파일 읽기 실패 시 처리
reader.onerror = error => {
reject(error);
};
// 3. 파일 읽기 시작 (ArrayBuffer 형태로)
reader.readAsArrayBuffer(file);
});
}
export function arrayBufferToBufferInClient(arrayBuffer: ArrayBuffer): Buffer {
return Buffer.from(arrayBuffer);
}
const ExcelSheetToJson = {
parse,
fileToArrayBufferInClient,
arrayBufferToBufferInClient,
};
export default ExcelSheetToJson;