# 엑셀 다운로드 개선 계획 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  
**다음 업데이트**: 구현 완료 후

