- 화면 편집기 컬럼 설정 기반 다운로드 (visible 컬럼만) - 필터 조건 적용 (필터링된 데이터만 다운로드) - 한글 라벨명 표시 (column_labels 테이블 조회) - Entity 조인 값 표시 (writer → writer_name 등) - 카테고리 타입 라벨 변환 (코드 → 라벨) - 멀티테넌시 보안 강화 (autoFilter: true) - 디버깅 로그 정리 변경된 파일: - frontend/lib/utils/buttonActions.ts - frontend/lib/registry/components/table-list/TableListComponent.tsx 관련 이슈: #엑셀다운로드개선
7.9 KiB
7.9 KiB
엑셀 다운로드 개선 계획 v2 (수정)
📋 문서 정보
- 작성일: 2025-01-10
- 작성자: AI Developer
- 버전: 2.0 (사용자 피드백 반영)
- 상태: 구현 대기
🎯 변경된 요구사항 (사용자 피드백)
사용자가 원하는 동작
- ❌ 선택된 행만 다운로드 기능 제거 (불필요)
- ✅ 항상 필터링된 전체 데이터 다운로드 (현재 화면 기준)
- ✅ 화면에 표시된 컬럼만 다운로드
- ✅ 컬럼 라벨(한글) 우선 사용
- ✅ 멀티테넌시 준수 (company_code 필터링)
현재 문제
- 🐛 행 선택 안 했을 때: "다운로드할 데이터가 없습니다" 에러
- ❌ 선택된 행만 다운로드: 사용자가 원하지 않는 동작
- ❌ 모든 컬럼 포함: 화면에 표시되지 않는 컬럼도 다운로드됨
- ❌ 필터 조건 무시: 사용자가 설정한 검색/필터가 적용되지 않음
- ❌ 멀티테넌시 위반: 모든 회사의 데이터를 가져올 가능성
🔄 수정된 다운로드 동작 흐름
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: 문서화
- 주석 추가
- 계획서 업데이트
- 커밋 메시지 작성
🚀 예상 효과
- 보안 강화: 멀티테넌시 100% 준수
- 사용자 경험 개선: 필터링된 전체 데이터 다운로드
- 직관적인 동작: 화면에 보이는 대로 다운로드
- 한글 지원: 컬럼 라벨명으로 엑셀 생성
🤝 승인
사용자 승인: ⬜ 대기 중
작성 완료: 2025-01-10
다음 업데이트: 구현 완료 후