feat: 파일프로세서 개선 - 안정적인 Excel 파일 처리
- 이전 잘 작동하던 코드 로직을 현재 프로세서에 적용 - LuckyExcel 우선 시도 + SheetJS Fallback 패턴 도입 - CSV, XLS, XLSX 모든 형식에 대한 안정적 처리 - 한글 시트명 정규화 및 워크북 구조 검증 강화 - 복잡한 SheetJS 옵션 단순화로 안정성 향상 - 에러 발생 시 빈 시트 생성으로 앱 중단 방지 - 테스트 환경 및 Cursor 규칙 업데이트 Technical improvements: - convertSheetJSToLuckyExcel 함수로 안정적 데이터 변환 - UTF-8 codepage 설정으로 한글 지원 강화 - validateWorkbook 함수로 방어적 프로그래밍 적용
This commit is contained in:
5
.cursor/rules/testing.mdc
Normal file
5
.cursor/rules/testing.mdc
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
195
.cursor/rules/xls_processing.mdc
Normal file
195
.cursor/rules/xls_processing.mdc
Normal file
@@ -0,0 +1,195 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# SheetJS Unified File Processing Rules
|
||||
|
||||
## **Critical Requirements for Excel/CSV File Processing**
|
||||
|
||||
- **Always use SheetJS XLSX.read() for all file types (CSV, XLS, XLSX)**
|
||||
- **Implement comprehensive Unicode/Korean support through proper codepage settings**
|
||||
- **Apply defensive programming patterns to prevent processing errors**
|
||||
- **Use optimized SheetJS options for performance and reliability**
|
||||
|
||||
## **SheetJS Unified Processing Chain**
|
||||
|
||||
### **1. File Type Detection and Format Handling**
|
||||
```typescript
|
||||
// ✅ DO: Detect file format and apply appropriate settings
|
||||
function getFileFormat(file: File): string {
|
||||
const extension = file.name.toLowerCase().split(".").pop();
|
||||
switch (extension) {
|
||||
case "csv": return "CSV";
|
||||
case "xls": return "XLS";
|
||||
case "xlsx": return "XLSX";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ DON'T: Use different processing chains for different formats
|
||||
function processXLSX(file) { /* LuckyExcel */ }
|
||||
function processXLS(file) { /* SheetJS conversion */ }
|
||||
function processCSV(file) { /* Custom parser */ }
|
||||
```
|
||||
|
||||
### **2. Korean/Unicode Support Configuration**
|
||||
```typescript
|
||||
// ✅ DO: Configure proper codepage for Korean support
|
||||
function getOptimalReadOptions(fileType: string): XLSX.ParsingOptions {
|
||||
const baseOptions = {
|
||||
type: "array",
|
||||
cellText: true, // Enable text generation
|
||||
raw: false, // Use formatted values (Korean guarantee)
|
||||
codepage: 65001, // UTF-8 codepage (Korean support)
|
||||
};
|
||||
|
||||
switch (fileType) {
|
||||
case "CSV":
|
||||
return { ...baseOptions, codepage: 65001 }; // UTF-8 for CSV
|
||||
case "XLS":
|
||||
return { ...baseOptions, codepage: 65001 }; // UTF-8 for XLS
|
||||
case "XLSX":
|
||||
return baseOptions; // XLSX natively supports UTF-8
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ DON'T: Ignore encoding settings
|
||||
const workbook = XLSX.read(data); // Missing encoding options
|
||||
```
|
||||
|
||||
### **3. Fallback Encoding Strategy**
|
||||
```typescript
|
||||
// ✅ DO: Implement fallback encoding for robust Korean support
|
||||
try {
|
||||
workbook = XLSX.read(arrayBuffer, { codepage: 65001 }); // UTF-8 first
|
||||
} catch (error) {
|
||||
// Fallback to appropriate encoding
|
||||
const fallbackCodepage = fileFormat === "CSV" ? 949 : 1252; // CP949 for Korean CSV
|
||||
workbook = XLSX.read(arrayBuffer, { codepage: fallbackCodepage });
|
||||
}
|
||||
|
||||
// ❌ DON'T: Give up on first encoding failure
|
||||
try {
|
||||
workbook = XLSX.read(arrayBuffer);
|
||||
} catch (error) {
|
||||
throw error; // No fallback strategy
|
||||
}
|
||||
```
|
||||
|
||||
### **4. Workbook Validation (Defensive Programming)**
|
||||
```typescript
|
||||
// ✅ DO: Validate workbook object thoroughly
|
||||
if (!workbook || typeof workbook !== "object") {
|
||||
throw new Error("워크북을 생성할 수 없습니다");
|
||||
}
|
||||
|
||||
if (!workbook.SheetNames || !Array.isArray(workbook.SheetNames) || workbook.SheetNames.length === 0) {
|
||||
throw new Error("시트 이름 정보가 없습니다");
|
||||
}
|
||||
|
||||
if (!workbook.Sheets || typeof workbook.Sheets !== "object") {
|
||||
throw new Error("유효한 시트가 없습니다");
|
||||
}
|
||||
|
||||
// ❌ DON'T: Skip validation
|
||||
const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; // Potential runtime error
|
||||
```
|
||||
|
||||
### **5. Optimized SheetJS Options**
|
||||
```typescript
|
||||
// ✅ DO: Use performance-optimized options
|
||||
const readOptions: XLSX.ParsingOptions = {
|
||||
type: "array",
|
||||
cellText: true, // ✅ Enable for Korean text
|
||||
cellNF: false, // ✅ Disable for performance
|
||||
cellHTML: false, // ✅ Disable for performance
|
||||
cellFormula: false, // ✅ Disable for performance
|
||||
cellStyles: false, // ✅ Disable for performance
|
||||
cellDates: true, // ✅ Enable for date handling
|
||||
sheetStubs: false, // ✅ Disable for performance
|
||||
bookProps: false, // ✅ Disable for performance
|
||||
bookSheets: true, // ✅ Enable for sheet info
|
||||
bookVBA: false, // ✅ Disable for performance
|
||||
raw: false, // ✅ Use formatted values (Korean guarantee)
|
||||
dense: false, // ✅ Use sparse arrays (memory efficient)
|
||||
WTF: false, // ✅ Ignore errors (stability)
|
||||
UTC: false, // ✅ Use local time
|
||||
};
|
||||
|
||||
// ❌ DON'T: Use default options without optimization
|
||||
const workbook = XLSX.read(data, { type: "array" }); // Missing optimizations
|
||||
```
|
||||
|
||||
### **6. Korean Data Processing**
|
||||
```typescript
|
||||
// ✅ DO: Process Korean data with proper settings
|
||||
const jsonData = XLSX.utils.sheet_to_json(sheet, {
|
||||
header: 1, // Array format
|
||||
defval: "", // Default value for empty cells
|
||||
blankrows: false, // Remove blank rows
|
||||
raw: false, // Use formatted values (Korean guarantee)
|
||||
});
|
||||
|
||||
// ❌ DON'T: Use raw values that might break Korean text
|
||||
const jsonData = XLSX.utils.sheet_to_json(sheet, {
|
||||
raw: true, // Might break Korean characters
|
||||
});
|
||||
```
|
||||
|
||||
## **Error Handling Patterns**
|
||||
|
||||
### **Specific Error Messages**
|
||||
```typescript
|
||||
// ✅ DO: Provide specific error messages
|
||||
if (arrayBuffer.byteLength === 0) {
|
||||
throw new Error(`${fileFormat} 파일이 비어있습니다.`);
|
||||
}
|
||||
|
||||
if (!workbook.SheetNames.length) {
|
||||
throw new Error("시트 이름 정보가 없습니다 - 파일이 비어있거나 손상되었습니다.");
|
||||
}
|
||||
|
||||
// ❌ DON'T: Use generic error messages
|
||||
throw new Error("File processing failed");
|
||||
```
|
||||
|
||||
## **Performance Guidelines**
|
||||
|
||||
- **Use `type: "array"` for ArrayBuffer input**
|
||||
- **Disable unnecessary features (cellFormula, cellStyles, etc.)**
|
||||
- **Use `raw: false` to ensure Korean text integrity**
|
||||
- **Enable `blankrows: false` to remove empty rows**
|
||||
- **Cache workbook objects when processing multiple sheets**
|
||||
|
||||
## **Testing Requirements**
|
||||
|
||||
- **Test with Korean filenames and Korean data content**
|
||||
- **Test encoding fallback scenarios (UTF-8 → CP949 → CP1252)**
|
||||
- **Test error handling for corrupted files**
|
||||
- **Test all supported file formats (CSV, XLS, XLSX)**
|
||||
- **Verify memory efficiency with large files**
|
||||
|
||||
## **Migration from LuckyExcel**
|
||||
|
||||
- **Replace all LuckyExcel transformExcelToLucky() calls with SheetJS**
|
||||
- **Remove XLS-to-XLSX conversion logic (SheetJS handles natively)**
|
||||
- **Update data structure from LuckyExcel format to standard JSON**
|
||||
- **Maintain backward compatibility in component interfaces**
|
||||
|
||||
## **Common Pitfalls to Avoid**
|
||||
|
||||
- ❌ Using different processing libraries for different file types
|
||||
- ❌ Ignoring codepage settings for Korean files
|
||||
- ❌ Not implementing encoding fallback strategies
|
||||
- ❌ Skipping workbook validation steps
|
||||
- ❌ Using `raw: true` which can break Korean characters
|
||||
- ❌ Not handling empty or corrupted files gracefully
|
||||
|
||||
## **Best Practices**
|
||||
|
||||
- ✅ Log processing steps for debugging Korean encoding issues
|
||||
- ✅ Use consistent error message format across all file types
|
||||
- ✅ Implement comprehensive test coverage for Korean scenarios
|
||||
- ✅ Monitor performance with large Korean datasets
|
||||
- ✅ Document encoding strategies for team knowledge sharing
|
||||
129
.cursor/rules/xlsx_debug.mdc
Normal file
129
.cursor/rules/xlsx_debug.mdc
Normal file
@@ -0,0 +1,129 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# XLSX 파일 처리 오류 방지 및 디버깅
|
||||
|
||||
## **문제 상황**
|
||||
- SheetJS로 읽은 XLSX 파일에서 `workbook.Sheets`가 undefined이거나 올바른 객체가 아닌 경우
|
||||
- "파일에 유효한 시트가 없습니다" 오류가 발생하는 경우
|
||||
- 한글 파일명이나 특수 문자가 포함된 XLSX 파일 처리 시 문제
|
||||
|
||||
## **필수 검증 단계**
|
||||
|
||||
### **1. 워크북 객체 검증**
|
||||
```typescript
|
||||
// ✅ DO: 워크북 구조 전체 디버깅
|
||||
console.log("🔍 워크북 구조 분석:");
|
||||
console.log("- workbook 존재:", !!workbook);
|
||||
console.log("- workbook 전체 키:", Object.keys(workbook));
|
||||
console.log("- workbook.Sheets 존재:", !!workbook.Sheets);
|
||||
console.log("- workbook.Sheets 타입:", typeof workbook.Sheets);
|
||||
|
||||
// ❌ DON'T: 단순한 truthy 체크만 하지 말 것
|
||||
if (workbook.Sheets) { ... }
|
||||
```
|
||||
|
||||
### **2. SheetJS 읽기 옵션 최적화**
|
||||
```typescript
|
||||
// ✅ DO: 안전한 SheetJS 옵션 사용
|
||||
workbook = XLSX.read(arrayBuffer, {
|
||||
type: "array",
|
||||
cellText: false, // 텍스트 셀 비활성화
|
||||
cellDates: true, // 날짜 형식 유지
|
||||
sheetStubs: false, // 빈 셀 스텁 비활성화
|
||||
codepage: 65001, // UTF-8 설정
|
||||
bookProps: false, // 워크북 속성 비활성화
|
||||
bookSheets: true, // 시트 정보만 활성화
|
||||
raw: false, // 원시 값 비활성화
|
||||
WTF: false // 엄격 모드 비활성화
|
||||
});
|
||||
|
||||
// ❌ DON'T: 기본 옵션만 사용하지 말 것
|
||||
workbook = XLSX.read(arrayBuffer);
|
||||
```
|
||||
|
||||
### **3. 단계별 오류 처리**
|
||||
```typescript
|
||||
// ✅ DO: 각 단계별 구체적 오류 메시지
|
||||
if (!workbook) {
|
||||
throw new Error("파일에서 워크북을 생성할 수 없습니다.");
|
||||
}
|
||||
|
||||
if (!workbook.Sheets || typeof workbook.Sheets !== "object") {
|
||||
console.error("❌ Sheets 속성 오류:", {
|
||||
exists: !!workbook.Sheets,
|
||||
type: typeof workbook.Sheets,
|
||||
value: workbook.Sheets
|
||||
});
|
||||
throw new Error("파일에 유효한 시트가 없습니다.");
|
||||
}
|
||||
|
||||
// ❌ DON'T: 일반적인 오류 메시지만 사용하지 말 것
|
||||
throw new Error("파일 처리 실패");
|
||||
```
|
||||
|
||||
### **4. 개별 시트 검증**
|
||||
```typescript
|
||||
// ✅ DO: 각 시트의 구조 확인
|
||||
workbook.SheetNames.forEach((sheetName, index) => {
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
console.log(`- 시트 [${index}] "${sheetName}":`, {
|
||||
exists: !!sheet,
|
||||
type: typeof sheet,
|
||||
keys: sheet ? Object.keys(sheet).slice(0, 5) : []
|
||||
});
|
||||
});
|
||||
|
||||
// ❌ DON'T: 시트 존재만 확인하고 내용 검증 생략하지 말 것
|
||||
const hasSheets = workbook.SheetNames.length > 0;
|
||||
```
|
||||
|
||||
## **한글 파일 처리 특수 사항**
|
||||
|
||||
### **1. 인코딩 처리**
|
||||
```typescript
|
||||
// ✅ DO: UTF-8 실패 시 CP949로 재시도 (XLS의 경우)
|
||||
try {
|
||||
workbook = XLSX.read(arrayBuffer, { codepage: 65001 }); // UTF-8
|
||||
} catch (error) {
|
||||
if (isXLS) {
|
||||
workbook = XLSX.read(arrayBuffer, { codepage: 949 }); // CP949
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **2. 파일명 및 시트명 검증**
|
||||
```typescript
|
||||
// ✅ DO: 한글 시트명 안전 처리
|
||||
const safeSheetName = sheetName || `Sheet${index + 1}`;
|
||||
console.log(`🔍 처리 중인 시트: "${safeSheetName}"`);
|
||||
|
||||
// ❌ DON'T: 시트명을 검증 없이 직접 사용하지 말 것
|
||||
console.log(`처리 중: ${sheetName}`);
|
||||
```
|
||||
|
||||
## **성능 최적화**
|
||||
|
||||
### **1. 메모리 효율적 처리**
|
||||
```typescript
|
||||
// ✅ DO: 큰 파일 처리 시 메모리 체크
|
||||
if (arrayBuffer.byteLength > 50 * 1024 * 1024) { // 50MB
|
||||
console.warn("⚠️ 대용량 파일 처리 중...");
|
||||
}
|
||||
|
||||
// ✅ DO: 변환 결과 검증
|
||||
if (!xlsxBuffer || xlsxBuffer.byteLength === 0) {
|
||||
throw new Error("XLSX 변환 결과가 비어있습니다.");
|
||||
}
|
||||
```
|
||||
|
||||
## **테스트 고려사항**
|
||||
- 한글 파일명이 포함된 XLSX 파일 테스트
|
||||
- 빈 시트가 포함된 파일 테스트
|
||||
- 손상된 XLSX 파일 테스트
|
||||
- 대용량 파일 처리 테스트
|
||||
- 특수 문자가 포함된 시트명 테스트
|
||||
|
||||
참고: [xls_processing.mdc](mdc:.cursor/rules/xls_processing.mdc)와 연계하여 사용
|
||||
Reference in New Issue
Block a user