diff --git a/src/main/java/egovframework/util/excel/BaseSxssfExcelFile.java b/src/main/java/egovframework/util/excel/BaseSxssfExcelFile.java index 84775a9..e03050f 100644 --- a/src/main/java/egovframework/util/excel/BaseSxssfExcelFile.java +++ b/src/main/java/egovframework/util/excel/BaseSxssfExcelFile.java @@ -16,25 +16,115 @@ import java.util.List; import static egovframework.util.excel.SuperClassReflectionUtils.getAllFieldsWithExcelColumn; +/** + * SXSSF(Streaming) 방식의 엑셀 파일 생성을 위한 추상 베이스 클래스 + * + *
이 클래스는 대용량 엑셀 파일 생성을 위해 Apache POI의 SXSSF(Streaming) API를 활용합니다. + * SXSSF는 메모리에 일정 개수의 행만 유지하고 나머지는 디스크에 flush하여 메모리 사용량을 최소화합니다.
+ * + *주요 기능:
+ *ROW_ACCESS_WINDOW_SIZE 크기의 SXSSF 워크북을 생성합니다.
+ */ public BaseSxssfExcelFile() { this.workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE); } + // ==================== 렌더링 메서드 ==================== + + /** + * 엑셀 시트에 제목, 헤더, 데이터를 모두 렌더링합니다. + * + *이 메서드는 엑셀 시트 생성의 전체 프로세스를 처리합니다:
+ *첫 번째 행에 제목을 생성하고, 모든 컬럼에 걸쳐 셀을 병합합니다. + * 제목 스타일은 굵은 22포인트 폰트, 왼쪽 정렬, 33포인트 행 높이로 설정됩니다.
* * @param sheetName 시트명 * @param title 제목 텍스트 - * @param columnCount 열 개수 + * @param columnCount 전체 컬럼 개수 */ protected void renderTitleRow(String sheetName, String title, int columnCount) { // 시트가 없으면 생성 @@ -42,149 +132,291 @@ public abstract class BaseSxssfExcelFile implements ExcelFile { sheet = workbook.createSheet(sheetName); } - // 제목 행 생성 (row 0) + // 제목 행 생성 Row titleRow = sheet.createRow(ROW_START_INDEX); + titleRow.setHeightInPoints(TITLE_ROW_HEIGHT); - // 제목 스타일 생성 (굵게, 가운데 정렬) - CellStyle titleStyle = workbook.createCellStyle(); - titleStyle.setAlignment(HorizontalAlignment.LEFT); - titleStyle.setVerticalAlignment(VerticalAlignment.CENTER); - - // 제목 폰트 생성 (맑은 고딕, 22 포인트, 굵게) - Font titleFont = workbook.createFont(); - //titleFont.setFontName("맑은 고딕"); 폰트 개이상함 - titleFont.setFontHeightInPoints((short) 22); - titleFont.setBold(true); - titleStyle.setFont(titleFont); - - // 제목 행 높이 설정 - titleRow.setHeightInPoints(33); + // 제목 스타일 생성 + CellStyle titleStyle = createTitleCellStyle(); // 첫 번째 셀에 제목 입력 createCell(titleRow, COLUMN_START_INDEX, title, titleStyle); - // 모든 열에 걸쳐 셀 병합 (row 0, column 0 ~ columnCount-1) + // 모든 컬럼에 걸쳐 셀 병합 if (columnCount > 1) { sheet.addMergedRegion(new CellRangeAddress( - ROW_START_INDEX, // 시작 행 - ROW_START_INDEX, // 종료 행 - COLUMN_START_INDEX, // 시작 열 - columnCount - 1 // 종료 열 + ROW_START_INDEX, // 시작 행 + ROW_START_INDEX, // 종료 행 + COLUMN_START_INDEX, // 시작 컬럼 + columnCount - 1 // 종료 컬럼 )); } - // 제목 행 오프셋 설정 + // 제목 행 오프셋 설정 (다음 행부터 헤더가 시작됨) titleRowOffset = 1; } + /** + * 헤더 행을 렌더링합니다. + * + *각 필드의 {@link ExcelColumn} 어노테이션에 정의된 헤더명을 사용하여 헤더 행을 생성합니다. + * 헤더는 굵은 폰트, 가운데 정렬로 표시됩니다.
+ * + *또한 SXSSF에서 autoSizeColumn을 사용하기 위해 각 컬럼을 추적 대상으로 등록합니다.
+ * + * @param excelMetadata 엑셀 메타데이터 (헤더명, 필드명, 시트명 포함) + */ protected void renderHeaders(ExcelMetadata excelMetadata) { - // 시트가 없으면 생성 (제목 행이 없는 경우) + // 시트가 없으면 생성 (제목이 없는 경우) if (sheet == null) { sheet = workbook.createSheet(excelMetadata.getSheetName()); } - // 헤더 행 생성 (제목 행이 있으면 row 1, 없으면 row 0) + // 헤더 행 생성 (제목이 있으면 row 1, 없으면 row 0) Row row = sheet.createRow(ROW_START_INDEX + titleRowOffset); + + // 헤더 스타일 생성 (굵게, 가운데 정렬) + CellStyle headerStyle = createHeaderCellStyle(); + + // 각 필드의 헤더명 렌더링 int columnIndex = COLUMN_START_INDEX; - CellStyle style = createCellStyle(workbook, true); - style.setAlignment(HorizontalAlignment.CENTER); for (String fieldName : excelMetadata.getDataFieldNames()) { - createCell(row, columnIndex++, excelMetadata.getHeaderName(fieldName), style); + createCell(row, columnIndex++, excelMetadata.getHeaderName(fieldName), headerStyle); } - // SXSSFWorkbook에서 autoSizeColumn을 사용하기 위해 각 열을 추적하도록 설정 - // 헤더 생성 직후 추적을 시작하여 헤더도 너비 계산에 포함되도록 함 - int columnCount = excelMetadata.getDataFieldNames().size(); - for (int i = 0; i < columnCount; i++) { - ((org.apache.poi.xssf.streaming.SXSSFSheet) sheet).trackColumnForAutoSizing(COLUMN_START_INDEX + i); - } + // SXSSF에서 autoSizeColumn 사용을 위한 컬럼 추적 등록 + trackColumnsForAutoSizing(excelMetadata.getDataFieldNames().size()); } + /** + * 데이터 행들을 렌더링합니다. + * + *리플렉션을 사용하여 각 데이터 객체의 {@link ExcelColumn} 어노테이션이 붙은 필드 값을 읽어 + * 엑셀 행으로 변환합니다.
+ * + *데이터 렌더링 후 각 컬럼의 너비를 조정합니다: + *
headerWidth가 지정된 경우 해당 값을 사용하고, + * 그렇지 않은 경우 자동 조정 후 한글 폰트 보정을 적용합니다.
+ * + * @param fields ExcelColumn 어노테이션이 있는 필드 리스트 + * @param metadata 엑셀 메타데이터 (헤더 너비 정보 포함) + */ + private void adjustColumnWidths(ListPOI의 autoSizeColumn은 한글 폰트 크기를 정확하게 계산하지 못하므로, + * 자동 조정 후 1.3배 증가시키고 최소/최대 범위를 적용합니다.
+ * + * @param columnIndex 컬럼 인덱스 + */ + private void autoSizeColumnWithKoreanFontCorrection(int columnIndex) { + sheet.autoSizeColumn(columnIndex); + + int currentWidth = sheet.getColumnWidth(columnIndex); + int adjustedWidth = (int) (currentWidth * AUTO_SIZE_MULTIPLIER); + + // 최소/최대 범위 적용 + int finalWidth = Math.max(MIN_COLUMN_WIDTH, Math.min(adjustedWidth, MAX_COLUMN_WIDTH)); + sheet.setColumnWidth(columnIndex, finalWidth); + } + + // ==================== ExcelFile 인터페이스 구현 ==================== + + /** + * 워크북을 출력 스트림에 씁니다. + * + * @param stream 출력 스트림 + * @throws RuntimeException IO 오류 발생 시 + */ @Override public void write(OutputStream stream) { try { workbook.write(stream); } catch (IOException e) { - throw new RuntimeException(e); + throw new RuntimeException("엑셀 파일 쓰기 중 오류가 발생했습니다.", e); } } + /** + * 워크북을 암호화하여 출력 스트림에 씁니다. + * + *AES Agile 암호화 방식을 사용합니다. + * password가 null인 경우 암호화 없이 일반 파일로 작성됩니다.
+ * + * @param stream 출력 스트림 + * @param password 암호화 비밀번호 (null이면 암호화하지 않음) + * @throws RuntimeException IO 오류 또는 암호화 오류 발생 시 + */ @Override public void writeWithEncryption(OutputStream stream, String password) { try { if (password == null) { write(stream); } else { - POIFSFileSystem fileSystem = new POIFSFileSystem(); - OutputStream encryptorStream = getEncryptorStream(fileSystem, password); - workbook.write(encryptorStream); - encryptorStream.close(); // this is necessary before writing out the FileSystem - fileSystem.writeFilesystem(stream); // write the encrypted file to the response stream - fileSystem.close(); + encryptAndWrite(stream, password); } workbook.close(); stream.close(); } catch (IOException e) { - throw new RuntimeException(e); + throw new RuntimeException("암호화된 엑셀 파일 쓰기 중 오류가 발생했습니다.", e); } } + /** + * 워크북을 암호화하여 출력 스트림에 씁니다. + * + * @param stream 출력 스트림 + * @param password 암호화 비밀번호 + * @throws IOException IO 오류 발생 시 + */ + private void encryptAndWrite(OutputStream stream, String password) throws IOException { + POIFSFileSystem fileSystem = new POIFSFileSystem(); + OutputStream encryptorStream = getEncryptorStream(fileSystem, password); + + workbook.write(encryptorStream); + encryptorStream.close(); + + fileSystem.writeFilesystem(stream); + fileSystem.close(); + } + + /** + * POIFSFileSystem에 대한 암호화 스트림을 생성합니다. + * + * @param fileSystem POI 파일 시스템 + * @param password 암호화 비밀번호 + * @return 암호화된 출력 스트림 + * @throws RuntimeException 암호화 초기화 실패 시 + */ private OutputStream getEncryptorStream(POIFSFileSystem fileSystem, String password) { try { Encryptor encryptor = new EncryptionInfo(EncryptionMode.agile).getEncryptor(); encryptor.confirmPassword(password); return encryptor.getDataStream(fileSystem); } catch (IOException | GeneralSecurityException e) { - throw new RuntimeException("Failed to obtain encrypted data stream from POIFSFileSystem."); + throw new RuntimeException("POIFSFileSystem에서 암호화 스트림 생성에 실패했습니다.", e); } } } - diff --git a/src/main/java/egovframework/util/excel/ExcelFile.java b/src/main/java/egovframework/util/excel/ExcelFile.java index 5b3ffc7..d632e6a 100644 --- a/src/main/java/egovframework/util/excel/ExcelFile.java +++ b/src/main/java/egovframework/util/excel/ExcelFile.java @@ -9,42 +9,119 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; -public interface ExcelFile { // (1) +/** + * 엑셀 파일 생성을 위한 기본 인터페이스 + * + *이 인터페이스는 엑셀 파일 생성에 필요한 기본 메서드를 정의합니다. + * 셀 생성, 셀 스타일 설정, 파일 쓰기 등의 공통 기능을 제공합니다.
+ * + *주요 기능:
+ *password가 null이면 암호화하지 않고 일반 파일로 작성됩니다.
+ * + * @param stream 출력 스트림 + * @param password 암호화 비밀번호 (null이면 암호화하지 않음) + * @throws IOException 파일 쓰기 또는 암호화 중 오류 발생 시 + */ void writeWithEncryption(OutputStream stream, String password) throws IOException; + // ==================== 셀 생성 메서드 ==================== + + /** + * 엑셀 셀을 생성하고 값과 스타일을 설정합니다. + * + *이 메서드는 다양한 타입의 데이터를 적절한 엑셀 셀 타입으로 변환합니다: + *
날짜/시간 포맷:
+ *기본적인 셀 스타일을 생성하며, 굵은 폰트 여부를 지정할 수 있습니다.
+ * + * @param wb 워크북 + * @param isBold 굵은 폰트 사용 여부 (true: 굵게, false: 보통) + * @return 생성된 셀 스타일 + */ default CellStyle createCellStyle(Workbook wb, boolean isBold) { CellStyle style = wb.createCellStyle(); Font font = wb.createFont(); diff --git a/src/main/java/egovframework/util/excel/ExcelMetadataFactory.java b/src/main/java/egovframework/util/excel/ExcelMetadataFactory.java index 18fc882..cb05c5a 100644 --- a/src/main/java/egovframework/util/excel/ExcelMetadataFactory.java +++ b/src/main/java/egovframework/util/excel/ExcelMetadataFactory.java @@ -6,41 +6,141 @@ import java.util.*; import static egovframework.util.excel.SuperClassReflectionUtils.getAllFields; import static org.springframework.core.annotation.AnnotationUtils.getAnnotation; -public class ExcelMetadataFactory { // (2) +/** + * 엑셀 메타데이터 생성을 위한 팩토리 클래스 (싱글톤) + * + *이 클래스는 VO 클래스의 {@link ExcelColumn} 및 {@link ExcelSheet} 어노테이션을 분석하여 + * 엑셀 파일 생성에 필요한 메타데이터를 생성합니다.
+ * + *주요 기능:
+ *사용 예제:
+ *{@code
+ * ExcelMetadata metadata = ExcelMetadataFactory.getInstance()
+ * .createMetadata(UserVO.class);
+ *
+ * String sheetName = metadata.getSheetName();
+ * List fieldNames = metadata.getDataFieldNames();
+ * }
+ *
+ * @see ExcelMetadata
+ * @see ExcelColumn
+ * @see ExcelSheet
+ * @author eGovFrame
+ */
+public class ExcelMetadataFactory {
+
+ // ==================== 싱글톤 구현 ====================
+
+ /**
+ * private 생성자 (외부에서 인스턴스 생성 방지)
+ */
private ExcelMetadataFactory() {
}
+ /**
+ * 싱글톤 인스턴스를 보유하는 내부 클래스
+ * Bill Pugh Singleton 패턴 사용 (Thread-safe, Lazy Loading)
+ */ private static class SingletonHolder { private static final ExcelMetadataFactory INSTANCE = new ExcelMetadataFactory(); } + /** + * 싱글톤 인스턴스를 반환합니다. + * + * @return ExcelMetadataFactory 싱글톤 인스턴스 + */ public static ExcelMetadataFactory getInstance() { return SingletonHolder.INSTANCE; } + // ==================== 메타데이터 생성 ==================== + + /** + * VO 클래스로부터 엑셀 메타데이터를 생성합니다. + * + *이 메서드는 VO 클래스의 모든 필드를 검사하여 {@link ExcelColumn} 어노테이션이 붙은 + * 필드만 추출하고, 각 필드의 헤더명과 컬럼 너비 정보를 수집합니다.
+ * + *처리 과정:
+ *{@link ExcelSheet} 어노테이션이 있으면 해당 시트명을 사용하고, + * 없으면 기본값 "Sheet1"을 반환합니다.
+ * + * @param clazz 시트명을 추출할 클래스 + * @return 시트명 (ExcelSheet 어노테이션의 name 또는 "Sheet1") + */ + private String extractSheetName(Class> clazz) { ExcelSheet annotation = getAnnotation(clazz, ExcelSheet.class); - if (annotation != null) { - return annotation.name(); - } - return "Sheet1"; + return annotation != null ? annotation.name() : "Sheet1"; } } diff --git a/src/main/java/egovframework/util/excel/ExcelSheetData.java b/src/main/java/egovframework/util/excel/ExcelSheetData.java index 94c28ca..7106747 100644 --- a/src/main/java/egovframework/util/excel/ExcelSheetData.java +++ b/src/main/java/egovframework/util/excel/ExcelSheetData.java @@ -5,17 +5,164 @@ import lombok.Getter; import java.util.List; +/** + * 엑셀 시트 데이터를 담는 DTO 클래스 + * + *이 클래스는 엑셀 파일 생성에 필요한 데이터와 메타정보를 캡슐화합니다. + * 데이터 리스트, VO 클래스 타입, 제목(선택사항)을 포함합니다.
+ * + *주요 특징:
+ *사용 예제 1: 제목 없는 엑셀 시트
+ *{@code
+ * // VO 리스트 조회
+ * List userList = userService.selectUserList();
+ *
+ * // ExcelSheetData 생성 (제목 없음)
+ * ExcelSheetData sheetData = ExcelSheetData.of(userList, UserVO.class);
+ *
+ * // 엑셀 파일 생성
+ * new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx");
+ * }
+ *
+ * 사용 예제 2: 제목이 있는 엑셀 시트
+ *{@code
+ * // VO 리스트 조회
+ * List userList = userService.selectUserList();
+ *
+ * // ExcelSheetData 생성 (제목 포함)
+ * ExcelSheetData sheetData = ExcelSheetData.of(
+ * userList,
+ * UserVO.class,
+ * "2024년 1월 사용자 목록" // 제목 행에 표시될 텍스트
+ * );
+ *
+ * // 엑셀 파일 생성 (첫 행에 제목이 표시됨)
+ * new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx");
+ * }
+ *
+ * 사용 예제 3: 다중 시트 엑셀
+ *{@code
+ * // 시트 1: 사용자 목록
+ * List userList = userService.selectUserList();
+ * ExcelSheetData userSheet = ExcelSheetData.of(userList, UserVO.class, "사용자 목록");
+ *
+ * // 시트 2: 부서 목록
+ * List deptList = deptService.selectDeptList();
+ * ExcelSheetData deptSheet = ExcelSheetData.of(deptList, DeptVO.class, "부서 목록");
+ *
+ * // 다중 시트 그룹 생성
+ * ExcelSheetDataGroup dataGroup = ExcelSheetDataGroup.of(
+ * List.of(userSheet, deptSheet)
+ * );
+ *
+ * // 다중 시트 엑셀 파일 생성
+ * new SxssfMultiSheetExcelFile(dataGroup, response);
+ * }
+ *
+ * @see SxssfExcelFile
+ * @see SxssfMultiSheetExcelFile
+ * @see ExcelSheetDataGroup
+ * @author eGovFrame
+ */
@Getter
@AllArgsConstructor
-public class ExcelSheetData { // (1)
+public class ExcelSheetData {
+
+ // ==================== 필드 ====================
+
+ /**
+ * 엑셀에 출력할 데이터 리스트
+ *
+ * 각 요소는 {@link ExcelColumn} 어노테이션이 붙은 VO 객체여야 합니다. + * 리스트의 각 객체가 엑셀의 한 행(row)으로 변환됩니다.
+ */ private final List> dataList; + + /** + * 데이터 리스트의 요소 타입 (VO 클래스) + * + *이 클래스는 다음 어노테이션을 가져야 합니다: + *
예제:
+ *{@code
+ * @ExcelSheet(name = "사용자")
+ * public class UserVO {
+ * @ExcelColumn(headerName = "이름", headerWidth = 20)
+ * private String userName;
+ *
+ * @ExcelColumn(headerName = "이메일")
+ * private String email;
+ * }
+ * }
+ */
private final Class> type;
- private final String title; // 제목 행 텍스트 (null이면 제목 행 생성하지 않음)
+ /**
+ * 제목 행 텍스트 (선택사항)
+ *
+ * 이 값이 null이 아니고 빈 문자열이 아니면, 엑셀 파일의 첫 번째 행에 + * 제목이 표시됩니다. 제목은 모든 컬럼에 걸쳐 병합된 셀로 표시되며, + * 굵은 22포인트 폰트로 렌더링됩니다.
+ * + *null이거나 빈 문자열이면 제목 행을 생성하지 않고, + * 첫 번째 행부터 헤더가 시작됩니다.
+ */ + private final String title; + + // ==================== 팩토리 메서드 ==================== + + /** + * 제목이 없는 ExcelSheetData 객체를 생성합니다. + * + *이 메서드로 생성한 객체는 제목 행 없이 헤더와 데이터만 출력됩니다.
+ * + *생성되는 엑셀 구조:
+ *+ * Row 0: [헤더1] [헤더2] [헤더3] ... + * Row 1: [데이터] [데이터] [데이터] ... + * Row 2: [데이터] [데이터] [데이터] ... + * ... + *+ * + * @param dataList 엑셀에 출력할 데이터 리스트 (null이거나 비어있어도 됨) + * @param type 데이터 리스트의 요소 타입 (VO 클래스, null이면 안됨) + * @return 생성된 ExcelSheetData 객체 + */ public static ExcelSheetData of(List> dataList, Class> type) { return new ExcelSheetData(dataList, type, null); } + /** + * 제목이 있는 ExcelSheetData 객체를 생성합니다. + * + *
이 메서드로 생성한 객체는 제목 행, 헤더 행, 데이터 행 순서로 출력됩니다. + * 제목은 첫 번째 행에 모든 컬럼에 걸쳐 병합되어 표시됩니다.
+ * + *생성되는 엑셀 구조:
+ *+ * Row 0: [제목 텍스트 - 병합된 셀] + * Row 1: [헤더1] [헤더2] [헤더3] ... + * Row 2: [데이터] [데이터] [데이터] ... + * Row 3: [데이터] [데이터] [데이터] ... + * ... + *+ * + * @param dataList 엑셀에 출력할 데이터 리스트 (null이거나 비어있어도 됨) + * @param type 데이터 리스트의 요소 타입 (VO 클래스, null이면 안됨) + * @param title 제목 행에 표시될 텍스트 (null이면 제목 행을 생성하지 않음) + * @return 생성된 ExcelSheetData 객체 + */ public static ExcelSheetData of(List> dataList, Class> type, String title) { return new ExcelSheetData(dataList, type, title); } diff --git a/src/main/java/egovframework/util/excel/SxssfExcelFile.java b/src/main/java/egovframework/util/excel/SxssfExcelFile.java index aa16676..9b702f3 100644 --- a/src/main/java/egovframework/util/excel/SxssfExcelFile.java +++ b/src/main/java/egovframework/util/excel/SxssfExcelFile.java @@ -2,7 +2,6 @@ package egovframework.util.excel; import org.checkerframework.checker.nullness.qual.Nullable; -import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @@ -11,67 +10,179 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +/** + * 단일 시트 엑셀 파일 생성 클래스 + * + *
이 클래스는 {@link BaseSxssfExcelFile}을 상속하여 단일 시트 엑셀 파일을 생성합니다. + * HTTP 응답으로 엑셀 파일을 다운로드하거나, OutputStream으로 파일을 출력할 수 있습니다.
+ * + *사용 예제 1: HTTP 응답으로 엑셀 다운로드
+ *{@code
+ * @GetMapping("/download.xlsx")
+ * public void downloadExcel(HttpServletRequest request, HttpServletResponse response) {
+ * List dataList = userService.selectUserList();
+ * ExcelSheetData sheetData = ExcelSheetData.of(dataList, UserVO.class, "사용자 목록");
+ * new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx");
+ * }
+ * }
+ *
+ * 사용 예제 2: 암호화된 엑셀 파일 다운로드
+ *{@code
+ * @GetMapping("/download-encrypted.xlsx")
+ * public void downloadEncryptedExcel(HttpServletRequest request, HttpServletResponse response) {
+ * List dataList = userService.selectUserList();
+ * ExcelSheetData sheetData = ExcelSheetData.of(dataList, UserVO.class, "사용자 목록");
+ * new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx", "password123");
+ * }
+ * }
+ *
+ * 사용 예제 3: OutputStream으로 출력
+ *{@code
+ * try (FileOutputStream fos = new FileOutputStream("output.xlsx")) {
+ * List dataList = userService.selectUserList();
+ * ExcelSheetData sheetData = ExcelSheetData.of(dataList, UserVO.class);
+ * new SxssfExcelFile(sheetData, fos, null);
+ * }
+ * }
+ *
+ * VO 클래스 예제:
+ *{@code
+ * @ExcelSheet(name = "사용자")
+ * public class UserVO {
+ * @ExcelColumn(headerName = "이름", headerWidth = 20)
+ * private String userName;
+ *
+ * @ExcelColumn(headerName = "이메일", headerWidth = 30)
+ * private String email;
+ *
+ * @ExcelColumn(headerName = "전화번호")
+ * private String phone;
+ * }
+ * }
+ *
+ * @see BaseSxssfExcelFile
+ * @see ExcelSheetData
+ * @see ExcelColumn
+ * @author eGovFrame
+ */
public class SxssfExcelFile extends BaseSxssfExcelFile {
+
+ // ==================== 생성자 (HTTP 응답 다운로드) ====================
+
+ /**
+ * HTTP 응답으로 엑셀 파일을 다운로드합니다 (암호화 없음).
+ *
+ * 이 생성자는 암호화 없이 엑셀 파일을 HTTP 응답으로 다운로드합니다. + * 파일명은 브라우저 종류에 따라 적절하게 인코딩됩니다.
+ * + * @param data 엑셀 시트 데이터 (데이터 리스트, 타입, 제목) + * @param request HTTP 요청 객체 (브라우저 정보 확인용) + * @param response HTTP 응답 객체 + * @param fileName 다운로드될 파일명 (확장자 포함, 예: "사용자목록.xlsx") + * @throws RuntimeException 파일 생성 또는 출력 중 오류 발생 시 + */ public SxssfExcelFile(ExcelSheetData data, HttpServletRequest request, HttpServletResponse response, String fileName) { this(data, request, response, fileName, null); } - public SxssfExcelFile(ExcelSheetData data, OutputStream outputStream, @Nullable String password) { - ExcelMetadata metadata = ExcelMetadataFactory.getInstance().createMetadata(data.getType()); - exportExcelFile(data, metadata, outputStream, password); - } - - private void exportExcelFile(ExcelSheetData data, ExcelMetadata metadata, OutputStream stream, - @Nullable String password) { - // 제목이 있으면 제목 행 먼저 렌더링 - if (data.getTitle() != null && !data.getTitle().trim().isEmpty()) { - int columnCount = metadata.getDataFieldNames().size(); - renderTitleRow(metadata.getSheetName(), data.getTitle(), columnCount); - } - renderHeaders(metadata); - renderDataLines(data, metadata); - writeWithEncryption(stream, password); // if password is null, encryption will not be applied. - } - + /** + * HTTP 응답으로 엑셀 파일을 다운로드합니다 (암호화 지원). + * + *이 생성자는 선택적으로 암호화하여 엑셀 파일을 HTTP 응답으로 다운로드합니다. + * 파일명은 브라우저 종류에 따라 적절하게 인코딩됩니다.
+ * + * @param data 엑셀 시트 데이터 (데이터 리스트, 타입, 제목) + * @param request HTTP 요청 객체 (브라우저 정보 확인용) + * @param response HTTP 응답 객체 + * @param fileName 다운로드될 파일명 (확장자 포함, 예: "사용자목록.xlsx") + * @param password 암호화 비밀번호 (null이면 암호화하지 않음) + * @throws RuntimeException 파일 생성 또는 출력 중 오류 발생 시 + */ public SxssfExcelFile(ExcelSheetData data, HttpServletRequest request, HttpServletResponse response, - String fileName, @Nullable String password) { + String fileName, @Nullable String password) { try { - setFileName(request, response, fileName); + setDownloadHeaders(request, response, fileName); ExcelMetadata metadata = ExcelMetadataFactory.getInstance().createMetadata(data.getType()); - exportExcelFile(data, metadata, response.getOutputStream(), password); + renderSheetContent(data, metadata); + writeWithEncryption(response.getOutputStream(), password); } catch (IOException e) { - throw new RuntimeException(e); + throw new RuntimeException("HTTP 응답으로 엑셀 파일 출력 중 오류가 발생했습니다.", e); } } - private void exportExcelFile(ExcelSheetData data, ExcelMetadata metadata, ServletOutputStream stream, - String password) { - // 제목이 있으면 제목 행 먼저 렌더링 - if (data.getTitle() != null && !data.getTitle().trim().isEmpty()) { - int columnCount = metadata.getDataFieldNames().size(); - renderTitleRow(metadata.getSheetName(), data.getTitle(), columnCount); - } - renderHeaders(metadata); - renderDataLines(data, metadata); - writeWithEncryption(stream, password); // if password is null, encryption will not be applied. + // ==================== 생성자 (OutputStream 출력) ==================== + + /** + * OutputStream으로 엑셀 파일을 출력합니다. + * + *이 생성자는 선택적으로 암호화하여 엑셀 파일을 OutputStream으로 출력합니다. + * 파일 시스템에 직접 저장하거나 다른 스트림 처리 용도로 사용할 수 있습니다.
+ * + * @param data 엑셀 시트 데이터 (데이터 리스트, 타입, 제목) + * @param outputStream 출력 스트림 + * @param password 암호화 비밀번호 (null이면 암호화하지 않음) + * @throws RuntimeException 파일 생성 또는 출력 중 오류 발생 시 + */ + public SxssfExcelFile(ExcelSheetData data, OutputStream outputStream, @Nullable String password) { + ExcelMetadata metadata = ExcelMetadataFactory.getInstance().createMetadata(data.getType()); + renderSheetContent(data, metadata); + writeWithEncryption(outputStream, password); } - private void setFileName(HttpServletRequest request, HttpServletResponse response, String fileName) { + // ==================== private 헬퍼 메서드 ==================== + + /** + * HTTP 응답 헤더를 설정하여 엑셀 파일 다운로드를 준비합니다. + * + *브라우저 종류에 따라 파일명을 적절하게 인코딩하여 Content-Disposition 헤더를 설정합니다: + *
이 클래스는 {@link BaseSxssfExcelFile}을 상속하여 여러 시트를 포함하는 엑셀 파일을 생성합니다. + * 하나의 엑셀 파일에 여러 종류의 데이터를 각각 다른 시트로 구성할 수 있습니다.
+ * + *사용 예제 1: 다중 시트 엑셀 다운로드
+ *{@code
+ * @GetMapping("/download-multi.xlsx")
+ * public void downloadMultiSheetExcel(HttpServletResponse response) throws IOException {
+ * // 시트 1: 사용자 목록
+ * List userList = userService.selectUserList();
+ * ExcelSheetData userSheet = ExcelSheetData.of(userList, UserVO.class, "사용자 목록");
+ *
+ * // 시트 2: 부서 목록
+ * List deptList = deptService.selectDeptList();
+ * ExcelSheetData deptSheet = ExcelSheetData.of(deptList, DeptVO.class, "부서 목록");
+ *
+ * // 다중 시트 그룹 생성
+ * ExcelSheetDataGroup dataGroup = ExcelSheetDataGroup.of(
+ * List.of(userSheet, deptSheet)
+ * );
+ *
+ * // 파일 다운로드
+ * response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ * response.setHeader("Content-Disposition", "attachment; filename=\"통합리포트.xlsx\"");
+ * new SxssfMultiSheetExcelFile(dataGroup, response);
+ * }
+ * }
+ *
+ * 사용 예제 2: 암호화된 다중 시트 엑셀 다운로드
+ *{@code
+ * @GetMapping("/download-multi-encrypted.xlsx")
+ * public void downloadEncryptedMultiSheetExcel(HttpServletResponse response) throws IOException {
+ * ExcelSheetDataGroup dataGroup = createMultiSheetData();
+ *
+ * response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ * response.setHeader("Content-Disposition", "attachment; filename=\"통합리포트.xlsx\"");
+ * new SxssfMultiSheetExcelFile(dataGroup, response, "password123");
+ * }
+ * }
+ *
+ * 주의사항:
+ *이 생성자는 암호화 없이 다중 시트 엑셀 파일을 HTTP 응답으로 다운로드합니다.
+ * + * @param dataGroup 다중 시트 데이터 그룹 + * @param response HTTP 응답 객체 + * @throws IOException 파일 생성 또는 출력 중 오류 발생 시 + */ + public SxssfMultiSheetExcelFile(ExcelSheetDataGroup dataGroup, HttpServletResponse response) + throws IOException { this(dataGroup, response, null); } + /** + * HTTP 응답으로 다중 시트 엑셀 파일을 다운로드합니다 (암호화 지원). + * + *이 생성자는 선택적으로 암호화하여 다중 시트 엑셀 파일을 HTTP 응답으로 다운로드합니다.
+ * + * @param dataGroup 다중 시트 데이터 그룹 + * @param response HTTP 응답 객체 + * @param password 암호화 비밀번호 (null이면 암호화하지 않음) + * @throws IOException 파일 생성 또는 출력 중 오류 발생 시 + */ public SxssfMultiSheetExcelFile(ExcelSheetDataGroup dataGroup, HttpServletResponse response, - @Nullable String password) throws IOException { - exportExcelFile(dataGroup, response.getOutputStream(), password); + @Nullable String password) throws IOException { + renderAllSheets(dataGroup); + writeWithEncryption(response.getOutputStream(), password); } - private void exportExcelFile(ExcelSheetDataGroup dataGroup, ServletOutputStream stream, String password) throws - IOException { + // ==================== private 헬퍼 메서드 ==================== + + /** + * 모든 시트를 렌더링합니다. + * + *데이터 그룹에 포함된 각 시트 데이터를 순회하면서 개별 시트를 생성합니다. + * 각 시트마다 제목, 헤더, 데이터 행을 렌더링하고 새로운 시트로 이동합니다.
+ * + * @param dataGroup 다중 시트 데이터 그룹 + */ + private void renderAllSheets(ExcelSheetDataGroup dataGroup) { for (ExcelSheetData data : dataGroup.getExcelSheetData()) { + // 현재 시트 참조 초기화 (새 시트 생성 준비) + resetSheetContext(); + + // 메타데이터 생성 ExcelMetadata metadata = ExcelMetadataFactory.getInstance().createMetadata(data.getType()); - // 제목이 있으면 제목 행 먼저 렌더링 - if (data.getTitle() != null && !data.getTitle().trim().isEmpty()) { - int columnCount = metadata.getDataFieldNames().size(); - renderTitleRow(metadata.getSheetName(), data.getTitle(), columnCount); - } - renderHeaders(metadata); - renderDataLines(data, metadata); + + // 시트 내용 렌더링 (제목, 헤더, 데이터) + renderSheetContent(data, metadata); } - writeWithEncryption(stream, password); // if password is null, encryption will not be applied. } -} + /** + * 시트 컨텍스트를 초기화합니다. + * + *새로운 시트 생성을 위해 현재 시트 참조와 제목 행 오프셋을 초기화합니다. + * 이렇게 하면 renderSheetContent 메서드가 새로운 시트를 생성하게 됩니다.
+ */ + private void resetSheetContext() { + sheet = null; + titleRowOffset = 0; + } +} diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnRegistAndViewController.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnRegistAndViewController.java index d8e0a4e..aa61387 100644 --- a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnRegistAndViewController.java +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/controller/CrdnRegistAndViewController.java @@ -416,37 +416,7 @@ public class CrdnRegistAndViewController { paramVO.setPagingYn("N"); // 단속 목록 조회 - List