ERP-node/docs/엑셀_다운로드_개선_계획_v2.md

276 lines
7.9 KiB
Markdown
Raw Permalink Normal View History

# 엑셀 다운로드 개선 계획 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<boolean> {
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<string, any> = {};
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
**다음 업데이트**: 구현 완료 후