# 엑셀 다운로드 개선 계획 v2 (수정) ## 📋 문서 정보 - **작성일**: 2025-01-10 - **작성자**: AI Developer - **버전**: 2.0 (사용자 피드백 반영) - **상태**: 구현 대기 --- ## 🎯 변경된 요구사항 (사용자 피드백) ### 사용자가 원하는 동작 1. ❌ **선택된 행만 다운로드 기능 제거** (불필요) 2. ✅ **항상 필터링된 전체 데이터 다운로드** (현재 화면 기준) 3. ✅ **화면에 표시된 컬럼만** 다운로드 4. ✅ **컬럼 라벨(한글) 우선** 사용 5. ✅ **멀티테넌시 준수** (company_code 필터링) ### 현재 문제 1. 🐛 **행 선택 안 했을 때**: "다운로드할 데이터가 없습니다" 에러 2. ❌ **선택된 행만 다운로드**: 사용자가 원하지 않는 동작 3. ❌ **모든 컬럼 포함**: 화면에 표시되지 않는 컬럼도 다운로드됨 4. ❌ **필터 조건 무시**: 사용자가 설정한 검색/필터가 적용되지 않음 5. ❌ **멀티테넌시 위반**: 모든 회사의 데이터를 가져올 가능성 --- ## 🔄 수정된 다운로드 동작 흐름 ### Before (현재 - 잘못된 동작) ``` 엑셀 다운로드 버튼 클릭 ↓ 1. 선택된 행이 있는가? ├─ Yes → 선택된 행만 다운로드 ❌ (사용자가 원하지 않음) └─ No → 현재 페이지 데이터만 (10개 등) ❌ (전체가 아님) ``` ### After (수정 - 올바른 동작) ``` 엑셀 다운로드 버튼 클릭 ↓ 🔒 멀티테넌시: company_code 자동 필터링 ↓ 🔍 필터 조건: 사용자가 설정한 검색/필터 적용 ↓ 📊 데이터 조회: 전체 필터링된 데이터 (최대 10,000개) ↓ 🎨 컬럼 필터링: 화면에 표시된 컬럼만 ↓ 🏷️ 라벨 적용: 컬럼명 → 한글 라벨명 ↓ 💾 엑셀 다운로드 ``` --- ## 🎯 수정된 데이터 우선순위 ### ❌ 제거: 선택된 행 다운로드 ```typescript // ❌ 삭제할 코드 if (context.selectedRowsData && context.selectedRowsData.length > 0) { dataToExport = context.selectedRowsData; // 불필요! } ``` ### ✅ 새로운 우선순위 ```typescript // ✅ 항상 API 호출로 전체 필터링된 데이터 가져오기 const response = await entityJoinApi.getTableDataWithJoins(context.tableName, { page: 1, size: 10000, // 최대 10,000개 sortBy: context.sortBy || "id", sortOrder: (context.sortOrder || "asc") as "asc" | "desc", search: context.filterConditions, // ✅ 필터 조건 searchTerm: context.searchTerm, // ✅ 검색어 autoFilter: true, // ✅ company_code 자동 필터링 (멀티테넌시) enableEntityJoin: true, // ✅ Entity 조인 (writer_name 등) }); dataToExport = response.data; // 필터링된 전체 데이터 ``` --- ## 📝 수정 사항 ### 1. `buttonActions.ts` - handleExcelDownload 리팩토링 **파일**: `frontend/lib/utils/buttonActions.ts` #### 변경 전 ```typescript // ❌ 잘못된 우선순위 if (context.selectedRowsData && context.selectedRowsData.length > 0) { dataToExport = context.selectedRowsData; // 선택된 행만 } else if (context.tableDisplayData && context.tableDisplayData.length > 0) { dataToExport = context.tableDisplayData; // 현재 페이지만 } ``` #### 변경 후 ```typescript private static async handleExcelDownload( config: ButtonActionConfig, context: ButtonActionContext ): Promise { try { let dataToExport: any[] = []; // ✅ 항상 API 호출로 필터링된 전체 데이터 가져오기 if (context.tableName) { const { entityJoinApi } = await import("@/lib/api/entityJoin"); // 🔒 멀티테넌시 준수: autoFilter로 company_code 자동 적용 const response = await entityJoinApi.getTableDataWithJoins(context.tableName, { page: 1, size: 10000, // 최대 10,000개 sortBy: context.sortBy || "id", sortOrder: (context.sortOrder || "asc") as "asc" | "desc", search: context.filterConditions, // ✅ 필터 조건 searchTerm: context.searchTerm, // ✅ 검색어 autoFilter: true, // ✅ company_code 자동 필터링 (멀티테넌시) enableEntityJoin: true, // ✅ Entity 조인 }); if (response.success && response.data) { dataToExport = response.data; } else { toast.error("데이터를 가져오는데 실패했습니다."); return false; } } else { toast.error("테이블 정보가 없습니다."); return false; } // 데이터가 없으면 종료 if (dataToExport.length === 0) { toast.error("다운로드할 데이터가 없습니다."); return false; } // 🎨 컬럼 필터링 및 라벨 적용 if (context.visibleColumns && context.visibleColumns.length > 0) { const visibleColumns = context.visibleColumns; const columnLabels = context.columnLabels || {}; dataToExport = dataToExport.map((row) => { const filteredRow: Record = {}; visibleColumns.forEach((columnName) => { // 라벨 우선 사용, 없으면 컬럼명 사용 const label = columnLabels[columnName] || columnName; filteredRow[label] = row[columnName]; }); return filteredRow; }); } // 💾 엑셀 파일 생성 const { exportToExcel } = await import("@/lib/utils/excelExport"); const fileName = config.excelFileName || `${context.tableName}_${new Date().toISOString().split("T")[0]}.xlsx`; const sheetName = config.excelSheetName || "Sheet1"; await exportToExcel(dataToExport, fileName, { sheetName, includeHeaders: config.excelIncludeHeaders !== false, }); toast.success(`${dataToExport.length}개 행이 다운로드되었습니다.`); return true; } catch (error) { console.error("엑셀 다운로드 오류:", error); toast.error("엑셀 다운로드 중 오류가 발생했습니다."); return false; } } ``` --- ## 🔒 보안 강화 (멀티테넌시) ### Before (위험) ```typescript // ❌ 모든 회사 데이터 노출 await dynamicFormApi.getTableData(tableName, { pageSize: 10000, // 필터 없음! }); ``` ### After (안전) ```typescript // ✅ 멀티테넌시 준수 await entityJoinApi.getTableDataWithJoins(tableName, { size: 10000, search: filterConditions, // 필터 조건 searchTerm: searchTerm, // 검색어 autoFilter: true, // company_code 자동 필터링 ✅ enableEntityJoin: true, // Entity 조인 ✅ }); ``` --- ## ✅ 구현 체크리스트 ### Step 1: handleExcelDownload 단순화 - [ ] 선택된 행 다운로드 로직 제거 (`context.selectedRowsData` 체크 삭제) - [ ] 화면 표시 데이터 로직 제거 (`context.tableDisplayData` 체크 삭제) - [ ] 항상 API 호출로 변경 (entityJoinApi.getTableDataWithJoins) - [ ] 멀티테넌시 필수 적용 (`autoFilter: true`) - [ ] 필터 조건 전달 (`search`, `searchTerm`) ### Step 2: 컬럼 필터링 및 라벨 적용 - [ ] `context.visibleColumns`로 필터링 - [ ] `context.columnLabels`로 라벨 변환 - [ ] 라벨 우선, 없으면 컬럼명 사용 ### Step 3: 테스트 - [ ] 필터 없이 다운로드 → 전체 데이터 (company_code 필터링) - [ ] 검색어 입력 후 다운로드 → 검색된 데이터만 - [ ] 필터 설정 후 다운로드 → 필터링된 데이터만 - [ ] 컬럼 숨기기 후 다운로드 → 표시된 컬럼만 - [ ] 멀티테넌시 테스트 → 다른 회사 데이터 안 보임 - [ ] 10,000개 제한 확인 ### Step 4: 문서화 - [ ] 주석 추가 - [ ] 계획서 업데이트 - [ ] 커밋 메시지 작성 --- ## 🚀 예상 효과 1. **보안 강화**: 멀티테넌시 100% 준수 2. **사용자 경험 개선**: 필터링된 전체 데이터 다운로드 3. **직관적인 동작**: 화면에 보이는 대로 다운로드 4. **한글 지원**: 컬럼 라벨명으로 엑셀 생성 --- ## 🤝 승인 **사용자 승인**: ⬜ 대기 중 --- **작성 완료**: 2025-01-10 **다음 업데이트**: 구현 완료 후