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

7.9 KiB

엑셀 다운로드 개선 계획 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개)
↓
🎨 컬럼 필터링: 화면에 표시된 컬럼만
↓
🏷️ 라벨 적용: 컬럼명 → 한글 라벨명
↓
💾 엑셀 다운로드

🎯 수정된 데이터 우선순위

제거: 선택된 행 다운로드

// ❌ 삭제할 코드
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
  dataToExport = context.selectedRowsData; // 불필요!
}

새로운 우선순위

// ✅ 항상 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

변경 전

// ❌ 잘못된 우선순위
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
  dataToExport = context.selectedRowsData; // 선택된 행만
}
else if (context.tableDisplayData && context.tableDisplayData.length > 0) {
  dataToExport = context.tableDisplayData; // 현재 페이지만
}

변경 후

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 (위험)

// ❌ 모든 회사 데이터 노출
await dynamicFormApi.getTableData(tableName, {
  pageSize: 10000, // 필터 없음!
});

After (안전)

// ✅ 멀티테넌시 준수
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
다음 업데이트: 구현 완료 후