From 26631eca439b1c88a6ec2cdf30cdc79fad8cb3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=98=81?= Date: Thu, 16 Oct 2025 10:44:05 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=91=EC=85=80=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C,=20=EC=A0=9C=EB=AA=A9=20=EB=84=A3=EC=9D=84?= =?UTF-8?q?=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80,=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=EC=9A=A9?= =?UTF-8?q?=EB=8F=84=20VO=20=EA=B8=B0=EC=B4=88=20=ED=8B=80=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20=EC=BF=BC=EB=A6=AC=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=8A=94=20=EC=88=98=EC=A0=95=20=EC=98=88=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/excel/BaseSxssfExcelFile.java | 376 ++++++++++++++---- .../egovframework/util/excel/ExcelFile.java | 99 ++++- .../util/excel/ExcelMetadataFactory.java | 122 +++++- .../util/excel/ExcelSheetData.java | 151 ++++++- .../util/excel/SxssfExcelFile.java | 197 +++++++-- .../util/excel/SxssfMultiSheetExcelFile.java | 127 +++++- .../CrdnRegistAndViewController.java | 32 +- .../main/mapper/CrdnRegistAndViewMapper.java | 9 + .../main/model/CrdnRegistAndViewExcelVO.java | 198 ++++++--- .../service/CrdnRegistAndViewService.java | 10 + .../impl/CrdnRegistAndViewServiceImpl.java | 18 + .../main/CrdnRegistAndViewMapper_maria.xml | 167 ++++++++ 12 files changed, 1272 insertions(+), 234 deletions(-) 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하여 메모리 사용량을 최소화합니다.

+ * + *

주요 기능:

+ * + * + * @see SxssfExcelFile + * @see SxssfMultiSheetExcelFile + * @author eGovFrame + */ public abstract class BaseSxssfExcelFile implements ExcelFile { + + // ==================== 상수 정의 ==================== + + /** SXSSF 워크북의 메모리 윈도우 크기 (메모리에 유지할 행의 개수) */ protected static final int ROW_ACCESS_WINDOW_SIZE = 10000; + + /** 엑셀 행 시작 인덱스 */ protected static final int ROW_START_INDEX = 0; + + /** 엑셀 컬럼 시작 인덱스 */ protected static final int COLUMN_START_INDEX = 0; + + /** 제목 폰트 크기 (포인트) */ + private static final short TITLE_FONT_SIZE = 22; + + /** 제목 행 높이 (포인트) */ + private static final float TITLE_ROW_HEIGHT = 33f; + + /** 자동 조정 시 컬럼 최소 너비 (POI 단위: 1/256 문자) */ + private static final int MIN_COLUMN_WIDTH = 3000; + + /** 자동 조정 시 컬럼 최대 너비 (POI 단위: 1/256 문자) */ + private static final int MAX_COLUMN_WIDTH = 15000; + + /** 자동 조정 시 너비 증가 비율 (한글 폰트 보정) */ + private static final double AUTO_SIZE_MULTIPLIER = 1.3; + + /** POI 컬럼 너비 단위 (1 문자 = 256 POI 단위) */ + private static final int POI_WIDTH_UNIT = 256; + + // ==================== 필드 ==================== + + /** SXSSF 워크북 인스턴스 */ protected SXSSFWorkbook workbook; + + /** 현재 작업 중인 시트 */ protected Sheet sheet; - protected int titleRowOffset = 0; // 제목 행이 있으면 1, 없으면 0 + /** 제목 행 오프셋 (제목이 있으면 1, 없으면 0) */ + protected int titleRowOffset = 0; + + // ==================== 생성자 ==================== + + /** + * 기본 생성자 + *

ROW_ACCESS_WINDOW_SIZE 크기의 SXSSF 워크북을 생성합니다.

+ */ public BaseSxssfExcelFile() { this.workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE); } + // ==================== 렌더링 메서드 ==================== + + /** + * 엑셀 시트에 제목, 헤더, 데이터를 모두 렌더링합니다. + * + *

이 메서드는 엑셀 시트 생성의 전체 프로세스를 처리합니다:

+ *
    + *
  1. 제목 행 렌더링 (제목이 있는 경우)
  2. + *
  3. 헤더 행 렌더링
  4. + *
  5. 데이터 행 렌더링
  6. + *
  7. 컬럼 너비 자동 조정
  8. + *
+ * + * @param data 엑셀 시트 데이터 (데이터 리스트, 타입, 제목 포함) + * @param metadata 엑셀 메타데이터 (헤더명, 컬럼 너비, 시트명 포함) + */ + protected void renderSheetContent(ExcelSheetData data, ExcelMetadata metadata) { + // 1. 제목 행 렌더링 (있는 경우) + if (data.getTitle() != null && !data.getTitle().trim().isEmpty()) { + int columnCount = metadata.getDataFieldNames().size(); + renderTitleRow(metadata.getSheetName(), data.getTitle(), columnCount); + } + + // 2. 헤더 행 렌더링 + renderHeaders(metadata); + + // 3. 데이터 행 렌더링 + renderDataLines(data, metadata); + } + /** * 제목 행을 렌더링합니다. - * 첫 번째 행에 제목을 생성하고 모든 열에 걸쳐 셀을 병합합니다. + * + *

첫 번째 행에 제목을 생성하고, 모든 컬럼에 걸쳐 셀을 병합합니다. + * 제목 스타일은 굵은 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} 어노테이션이 붙은 필드 값을 읽어 + * 엑셀 행으로 변환합니다.

+ * + *

데이터 렌더링 후 각 컬럼의 너비를 조정합니다: + *

+ *

+ * + * @param data 엑셀 시트 데이터 (데이터 리스트, 타입) + * @param metadata 엑셀 메타데이터 (헤더 너비 정보 포함) + * @throws RuntimeException 필드 접근 실패 시 + */ protected void renderDataLines(ExcelSheetData data, ExcelMetadata metadata) { - CellStyle style = createCellStyle(workbook, false); - // 데이터 시작 행 (제목 행이 있으면 row 2, 없으면 row 1) + CellStyle dataStyle = createCellStyle(workbook, false); + + // 데이터 시작 행 (제목이 있으면 row 2, 없으면 row 1) int rowIndex = ROW_START_INDEX + titleRowOffset + 1; List fields = getAllFieldsWithExcelColumn(data.getType()); - // 데이터 행 생성 + // 각 데이터 객체를 행으로 변환 for (Object record : data.getDataList()) { Row row = sheet.createRow(rowIndex++); - int columnIndex = COLUMN_START_INDEX; - try { - for (Field field : fields) { - field.setAccessible(true); - createCell(row, columnIndex++, field.get(record), style); - } - } catch (IllegalAccessException e) { - throw new RuntimeException("Error accessing data field rendering data lines.", e); + renderDataRow(row, record, fields, dataStyle); + } + + // 컬럼 너비 조정 + adjustColumnWidths(fields, metadata); + } + + // ==================== private 헬퍼 메서드 ==================== + + /** + * 제목 셀 스타일을 생성합니다. + * + * @return 제목용 CellStyle (굵은 22포인트 폰트, 왼쪽/중앙 정렬) + */ + private CellStyle createTitleCellStyle() { + CellStyle titleStyle = workbook.createCellStyle(); + titleStyle.setAlignment(HorizontalAlignment.LEFT); + titleStyle.setVerticalAlignment(VerticalAlignment.CENTER); + + Font titleFont = workbook.createFont(); + titleFont.setFontHeightInPoints(TITLE_FONT_SIZE); + titleFont.setBold(true); + titleStyle.setFont(titleFont); + + return titleStyle; + } + + /** + * 헤더 셀 스타일을 생성합니다. + * + * @return 헤더용 CellStyle (굵은 폰트, 가운데 정렬) + */ + private CellStyle createHeaderCellStyle() { + CellStyle headerStyle = createCellStyle(workbook, true); + headerStyle.setAlignment(HorizontalAlignment.CENTER); + return headerStyle; + } + + /** + * SXSSF autoSizeColumn 사용을 위해 컬럼들을 추적 대상으로 등록합니다. + * + * @param columnCount 추적할 컬럼 개수 + */ + private void trackColumnsForAutoSizing(int columnCount) { + for (int i = 0; i < columnCount; i++) { + ((org.apache.poi.xssf.streaming.SXSSFSheet) sheet) + .trackColumnForAutoSizing(COLUMN_START_INDEX + i); + } + } + + /** + * 데이터 행의 각 셀을 렌더링합니다. + * + * @param row 렌더링할 행 + * @param record 데이터 객체 + * @param fields ExcelColumn 어노테이션이 있는 필드 리스트 + * @param style 셀 스타일 + * @throws RuntimeException 필드 접근 실패 시 + */ + private void renderDataRow(Row row, Object record, List fields, CellStyle style) { + int columnIndex = COLUMN_START_INDEX; + try { + for (Field field : fields) { + field.setAccessible(true); + createCell(row, columnIndex++, field.get(record), style); } + } catch (IllegalAccessException e) { + throw new RuntimeException("데이터 필드 접근 중 오류가 발생했습니다.", e); } + } - // 각 열의 너비 조정 - // (renderHeaders에서 이미 trackColumnForAutoSizing 호출됨) + /** + * 각 컬럼의 너비를 조정합니다. + * + *

headerWidth가 지정된 경우 해당 값을 사용하고, + * 그렇지 않은 경우 자동 조정 후 한글 폰트 보정을 적용합니다.

+ * + * @param fields ExcelColumn 어노테이션이 있는 필드 리스트 + * @param metadata 엑셀 메타데이터 (헤더 너비 정보 포함) + */ + private void adjustColumnWidths(List fields, ExcelMetadata metadata) { for (int i = 0; i < fields.size(); i++) { int columnIndex = COLUMN_START_INDEX + i; Field field = fields.get(i); String fieldName = field.getName(); - // headerWidth가 지정되어 있으면 해당 값 사용, 없으면 자동 조정 int headerWidth = metadata.getHeaderWidth(fieldName); + if (headerWidth > 0) { - // headerWidth가 지정된 경우 해당 값으로 설정 (POI 단위: 1/256 문자) - sheet.setColumnWidth(columnIndex, headerWidth * 256); + // headerWidth가 지정된 경우: 지정된 값 사용 + setColumnWidthInCharacters(columnIndex, headerWidth); } else { - // headerWidth가 0이면 자동 조정 - sheet.autoSizeColumn(columnIndex); - - // 한글 폰트는 autoSizeColumn이 정확하지 않으므로 여유분 추가 - // 현재 너비의 1.3배로 설정 (최소 3000, 최대 15000) - int currentWidth = sheet.getColumnWidth(columnIndex); - int newWidth = (int) (currentWidth * 1.3); - - // 최소 너비 3000 (약 11.7문자), 최대 너비 15000 (약 58.6문자) - newWidth = Math.max(3000, Math.min(newWidth, 15000)); - sheet.setColumnWidth(columnIndex, newWidth); + // headerWidth가 0인 경우: 자동 조정 + 한글 보정 + autoSizeColumnWithKoreanFontCorrection(columnIndex); } } } + /** + * 컬럼 너비를 문자 단위로 설정합니다. + * + * @param columnIndex 컬럼 인덱스 + * @param widthInCharacters 문자 단위 너비 + */ + private void setColumnWidthInCharacters(int columnIndex, int widthInCharacters) { + sheet.setColumnWidth(columnIndex, widthInCharacters * POI_WIDTH_UNIT); + } + + /** + * 컬럼 너비를 자동 조정하고 한글 폰트 보정을 적용합니다. + * + *

POI의 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) +/** + * 엑셀 파일 생성을 위한 기본 인터페이스 + * + *

이 인터페이스는 엑셀 파일 생성에 필요한 기본 메서드를 정의합니다. + * 셀 생성, 셀 스타일 설정, 파일 쓰기 등의 공통 기능을 제공합니다.

+ * + *

주요 기능:

+ *
    + *
  • 다양한 타입의 데이터를 엑셀 셀로 변환
  • + *
  • 셀 스타일 생성 (굵기, 폰트 등)
  • + *
  • 엑셀 파일 출력 (일반/암호화)
  • + *
+ * + * @see BaseSxssfExcelFile + * @author eGovFrame + */ +public interface ExcelFile { + + // ==================== 파일 출력 메서드 ==================== + + /** + * 엑셀 워크북을 출력 스트림에 씁니다. + * + * @param stream 출력 스트림 + * @throws IOException 파일 쓰기 중 오류 발생 시 + */ void write(OutputStream stream) throws IOException; + /** + * 엑셀 워크북을 암호화하여 출력 스트림에 씁니다. + * + *

password가 null이면 암호화하지 않고 일반 파일로 작성됩니다.

+ * + * @param stream 출력 스트림 + * @param password 암호화 비밀번호 (null이면 암호화하지 않음) + * @throws IOException 파일 쓰기 또는 암호화 중 오류 발생 시 + */ void writeWithEncryption(OutputStream stream, String password) throws IOException; + // ==================== 셀 생성 메서드 ==================== + + /** + * 엑셀 셀을 생성하고 값과 스타일을 설정합니다. + * + *

이 메서드는 다양한 타입의 데이터를 적절한 엑셀 셀 타입으로 변환합니다: + *

    + *
  • 숫자 타입 (Integer, Long, Double, Float): 숫자 셀
  • + *
  • 불린 타입 (Boolean): 불린 셀
  • + *
  • 날짜/시간 타입 (LocalDateTime, LocalDate, LocalTime): 포맷된 문자열 셀
  • + *
  • 기타 타입: toString() 결과를 문자열 셀로 저장
  • + *
+ *

+ * + *

날짜/시간 포맷:

+ *
    + *
  • LocalDateTime: "yyyy-MM-dd HH:mm:ss"
  • + *
  • LocalDate: "yyyy-MM-dd"
  • + *
  • LocalTime: "HH:mm:ss"
  • + *
+ * + * @param 값의 타입 + * @param row 셀을 생성할 행 + * @param column 셀의 컬럼 인덱스 (0부터 시작) + * @param value 셀에 저장할 값 (null이면 셀을 생성하지 않음) + * @param style 셀에 적용할 스타일 + */ default void createCell(Row row, int column, T value, CellStyle style) { + // null 값은 빈 셀로 처리 (NPE 방지) if (value == null) { - return; // avoid NPE + return; } + Cell cell = row.createCell(column); + + // 타입별로 적절한 셀 값 설정 if (value instanceof Integer) { - cell.setCellValue((Integer)value); + cell.setCellValue((Integer) value); } else if (value instanceof Long) { - cell.setCellValue((Long)value); + cell.setCellValue((Long) value); } else if (value instanceof Double) { - cell.setCellValue((Double)value); + cell.setCellValue((Double) value); } else if (value instanceof Float) { - cell.setCellValue((Float)value); + cell.setCellValue((Float) value); } else if (value instanceof Boolean) { - cell.setCellValue((Boolean)value); + cell.setCellValue((Boolean) value); } else if (value instanceof LocalDateTime) { - LocalDateTime dateTime = (LocalDateTime)value; + LocalDateTime dateTime = (LocalDateTime) value; cell.setCellValue(dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); } else if (value instanceof LocalDate) { - LocalDate date = (LocalDate)value; + LocalDate date = (LocalDate) value; cell.setCellValue(date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); } else if (value instanceof LocalTime) { - LocalTime time = (LocalTime)value; + LocalTime time = (LocalTime) value; cell.setCellValue(time.format(DateTimeFormatter.ofPattern("HH:mm:ss"))); } else { cell.setCellValue(value.toString()); } - cell.setCellStyle(style); + // 스타일 적용 + cell.setCellStyle(style); } + // ==================== 스타일 생성 메서드 ==================== + + /** + * 셀 스타일을 생성합니다. + * + *

기본적인 셀 스타일을 생성하며, 굵은 폰트 여부를 지정할 수 있습니다.

+ * + * @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} 어노테이션을 분석하여 + * 엑셀 파일 생성에 필요한 메타데이터를 생성합니다.

+ * + *

주요 기능:

+ *
    + *
  • 리플렉션을 사용하여 VO 클래스의 필드 정보 추출
  • + *
  • ExcelColumn 어노테이션에서 헤더명과 컬럼 너비 수집
  • + *
  • 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} 어노테이션이 붙은 + * 필드만 추출하고, 각 필드의 헤더명과 컬럼 너비 정보를 수집합니다.

+ * + *

처리 과정:

+ *
    + *
  1. 상속 계층 구조를 포함한 모든 필드 탐색
  2. + *
  3. ExcelColumn 어노테이션이 있는 필드만 선택
  4. + *
  5. 헤더명, 컬럼 너비, 필드명 수집
  6. + *
  7. ExcelSheet 어노테이션에서 시트명 추출
  8. + *
  9. ExcelMetadata 객체 생성 및 반환
  10. + *
+ * + * @param clazz 메타데이터를 생성할 VO 클래스 + * @return 생성된 엑셀 메타데이터 + * @throws RuntimeException ExcelColumn 어노테이션이 하나도 없는 경우 + */ public ExcelMetadata createMetadata(Class clazz) { + // 헤더명, 컬럼 너비, 필드명을 저장할 컬렉션 (순서 유지) Map headerNamesMap = new LinkedHashMap<>(); Map headerWidthsMap = new LinkedHashMap<>(); List dataFieldNamesList = new ArrayList<>(); + + // 모든 필드를 탐색하여 ExcelColumn 어노테이션 정보 수집 for (Field field : getAllFields(clazz)) { if (field.isAnnotationPresent(ExcelColumn.class)) { ExcelColumn columnAnnotation = field.getAnnotation(ExcelColumn.class); - headerNamesMap.put(field.getName(), Objects.requireNonNull(columnAnnotation).headerName()); - headerWidthsMap.put(field.getName(), columnAnnotation.headerWidth()); - dataFieldNamesList.add(field.getName()); + String fieldName = field.getName(); + + // 헤더명, 컬럼 너비, 필드명 저장 + headerNamesMap.put(fieldName, Objects.requireNonNull(columnAnnotation).headerName()); + headerWidthsMap.put(fieldName, columnAnnotation.headerWidth()); + dataFieldNamesList.add(fieldName); } } + + // ExcelColumn 어노테이션이 하나도 없으면 예외 발생 + validateHasExcelColumns(clazz, headerNamesMap); + + // 메타데이터 객체 생성 및 반환 + return new ExcelMetadata( + headerNamesMap, + headerWidthsMap, + dataFieldNamesList, + extractSheetName(clazz) + ); + } + + // ==================== private 헬퍼 메서드 ==================== + + /** + * ExcelColumn 어노테이션이 하나 이상 있는지 검증합니다. + * + * @param clazz 검증할 클래스 + * @param headerNamesMap 수집된 헤더명 맵 + * @throws RuntimeException ExcelColumn 어노테이션이 하나도 없는 경우 + */ + private void validateHasExcelColumns(Class clazz, Map headerNamesMap) { if (headerNamesMap.isEmpty()) { - throw new RuntimeException(String.format("Class %s has not @ExcelColumn at all", clazz)); + throw new RuntimeException( + String.format("클래스 %s에 @ExcelColumn 어노테이션이 하나도 없습니다.", clazz.getName()) + ); } - return new ExcelMetadata(headerNamesMap, headerWidthsMap, dataFieldNamesList, getSheetName(clazz)); } - private String getSheetName(Class clazz) { + /** + * 클래스에서 시트명을 추출합니다. + * + *

{@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 클래스 타입, 제목(선택사항)을 포함합니다.

+ * + *

주요 특징:

+ *
    + *
  • 불변(Immutable) 객체 - 모든 필드가 final
  • + *
  • 팩토리 메서드 패턴 - of() 메서드로 객체 생성
  • + *
  • 제목 행 지원 - 선택적으로 엑셀 상단에 제목 행 추가 가능
  • + *
+ * + *

사용 예제 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 클래스) + * + *

이 클래스는 다음 어노테이션을 가져야 합니다: + *

    + *
  • {@link ExcelColumn} - 각 필드에 붙여서 헤더명과 컬럼 너비 지정
  • + *
  • {@link ExcelSheet} - 클래스에 붙여서 시트명 지정 (선택사항)
  • + *
+ *

+ * + *

예제:

+ *
{@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 헤더를 설정합니다: + *

    + *
  • IE (MSIE/Trident): UTF-8 URL 인코딩 (+ 공백 처리)
  • + *
  • 기타 브라우저: UTF-8 → ISO-8859-1 변환
  • + *
+ *

+ * + * @param request HTTP 요청 객체 (User-Agent 헤더 확인용) + * @param response HTTP 응답 객체 + * @param fileName 다운로드될 파일명 + * @throws RuntimeException 인코딩 오류 발생 시 + */ + private void setDownloadHeaders(HttpServletRequest request, HttpServletResponse response, String fileName) { try { String browser = request.getHeader("User-Agent"); - String encodedFileName; - if (browser.contains("MSIE") || browser.contains("Trident")) { - // IE 브라우저 대응 - encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); - } else { - // IE 외 브라우저 대응 (크롬, 파이어폭스 등) - encodedFileName = new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1); - } - // Content-Disposition 설정 + String encodedFileName = encodeFileName(fileName, browser); + response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\""); } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e.getMessage()); + throw new RuntimeException("파일명 인코딩 중 오류가 발생했습니다.", e); } } + + /** + * 브라우저에 맞게 파일명을 인코딩합니다. + * + * @param fileName 원본 파일명 + * @param userAgent User-Agent 헤더 값 + * @return 인코딩된 파일명 + * @throws UnsupportedEncodingException 인코딩 실패 시 + */ + private String encodeFileName(String fileName, String userAgent) throws UnsupportedEncodingException { + if (isInternetExplorer(userAgent)) { + // IE: UTF-8 URL 인코딩 (+ -> 공백 처리) + return URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); + } else { + // Chrome, Firefox 등: UTF-8 바이트를 ISO-8859-1로 변환 + return new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1); + } + } + + /** + * User-Agent에서 Internet Explorer 브라우저인지 확인합니다. + * + * @param userAgent User-Agent 헤더 값 + * @return IE인 경우 true, 아니면 false + */ + private boolean isInternetExplorer(String userAgent) { + return userAgent != null && (userAgent.contains("MSIE") || userAgent.contains("Trident")); + } } diff --git a/src/main/java/egovframework/util/excel/SxssfMultiSheetExcelFile.java b/src/main/java/egovframework/util/excel/SxssfMultiSheetExcelFile.java index abee115..51c9852 100644 --- a/src/main/java/egovframework/util/excel/SxssfMultiSheetExcelFile.java +++ b/src/main/java/egovframework/util/excel/SxssfMultiSheetExcelFile.java @@ -2,33 +2,128 @@ package egovframework.util.excel; import org.checkerframework.checker.nullness.qual.Nullable; -import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -public class SxssfMultiSheetExcelFile extends BaseSxssfExcelFile { // (4) - public SxssfMultiSheetExcelFile(ExcelSheetDataGroup dataGroup, HttpServletResponse response) throws IOException { +/** + * 다중 시트 엑셀 파일 생성 클래스 + * + *

이 클래스는 {@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");
+ * }
+ * }
+ * + *

주의사항:

+ *
    + *
  • 각 시트는 서로 다른 VO 클래스를 사용할 수 있습니다.
  • + *
  • 각 VO 클래스마다 {@link ExcelSheet} 어노테이션으로 시트명을 지정해야 합니다.
  • + *
  • 시트가 많을수록 메모리 사용량이 증가할 수 있습니다.
  • + *
+ * + * @see BaseSxssfExcelFile + * @see ExcelSheetDataGroup + * @see ExcelSheetData + * @author eGovFrame + */ +public class SxssfMultiSheetExcelFile extends BaseSxssfExcelFile { + + // ==================== 생성자 ==================== + + /** + * HTTP 응답으로 다중 시트 엑셀 파일을 다운로드합니다 (암호화 없음). + * + *

이 생성자는 암호화 없이 다중 시트 엑셀 파일을 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 list = service.selectList(paramVO); - - // VO를 ExcelVO로 변환 - List excelList = list.stream() - .map(vo -> CrdnRegistAndViewExcelVO.builder() - .crdnYr(vo.getCrdnYr()) - .crdnNo(vo.getCrdnNo()) - .stdgEmdCdNm(vo.getStdgEmdCdNm()) - .rgnSeCdNm(vo.getRgnSeCdNm()) - .dsclMthdCdNm(vo.getDsclMthdCdNm()) - .dsclYmd(vo.getDsclYmd()) - .exmnr(vo.getExmnr()) - .relevyYn(vo.getRelevyYn()) - .agrvtnLevyTrgtYn(vo.getAgrvtnLevyTrgtYn()) - .crdnPrcsSttsCdNm(vo.getCrdnPrcsSttsCdNm()) - .actCmpltCd(vo.getActCmpltCd()) - .lotnoWholAddr(vo.getLotnoWholAddr()) - .actTypeCdNm(vo.getActTypeCdNm()) - .usgIdxCdNm(vo.getUsgIdxCdNm()) - .ownrNams(vo.getOwnrNams()) - .actrNams(vo.getActrNams()) - .dspsBfhdBgngYmd(vo.getDspsBfhdBgngYmd()) - .crcCmdBgngYmd(vo.getCrcCmdBgngYmd()) - .crcUrgBgngYmd(vo.getCrcUrgBgngYmd()) - .levyPrvntcBgngYmd(vo.getLevyPrvntcBgngYmd()) - .levyBgngYmd(vo.getLevyBgngYmd()) - .payUrgBgngYmd(vo.getPayUrgBgngYmd()) - .regDt(vo.getRegDt() != null ? vo.getRegDt().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null) - .rgtrNm(vo.getRgtrNm()) - .build()) - .collect(Collectors.toList()); + List excelList = service.selectListForExcel(paramVO); // 엑셀 파일 생성 및 다운로드 (제목 행 포함) String filename = "단속목록_" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx"; diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/mapper/CrdnRegistAndViewMapper.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/mapper/CrdnRegistAndViewMapper.java index f564a70..02f6f3c 100644 --- a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/mapper/CrdnRegistAndViewMapper.java +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/mapper/CrdnRegistAndViewMapper.java @@ -1,5 +1,6 @@ package go.kr.project.crdn.crndRegistAndView.main.mapper; +import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewExcelVO; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewVO; import org.apache.ibatis.annotations.Mapper; @@ -106,4 +107,12 @@ public interface CrdnRegistAndViewMapper { */ String selectActCmpltCd(CrdnRegistAndViewVO vo); + /** + * 엑셀 다운로드용 단속 목록을 조회한다. + * 중요로직: 기존 selectList와 동일하되, 부과금액(LEVY_AMT), 향후절차(NEXT_PRCS_NM) 등 Excel 전용 컬럼 추가 + * @param vo 검색 조건을 담은 VO 객체 + * @return 엑셀 다운로드용 단속 목록 + */ + List selectListForExcel(CrdnRegistAndViewVO vo); + } \ No newline at end of file diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/model/CrdnRegistAndViewExcelVO.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/model/CrdnRegistAndViewExcelVO.java index 4346f0e..abf16e0 100644 --- a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/model/CrdnRegistAndViewExcelVO.java +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/model/CrdnRegistAndViewExcelVO.java @@ -1,6 +1,7 @@ package go.kr.project.crdn.crndRegistAndView.main.model; import egovframework.util.excel.ExcelColumn; +import egovframework.util.excel.ExcelSheet; import lombok.*; /** @@ -10,6 +11,7 @@ import lombok.*; * date : 2025-08-25 * description : 단속 목록 엑셀 다운로드용 VO 클래스 * 중요한 로직 주석: 엑셀 다운로드 시 사용되는 전용 VO로 @ExcelColumn 어노테이션을 포함 + * 엑셀 샘플 순서에 맞게 필드를 정렬하고, 필요한 컬럼만 헤더 설정 * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- @@ -20,77 +22,177 @@ import lombok.*; @AllArgsConstructor @NoArgsConstructor @ToString +@ExcelSheet(name = "단속목록") public class CrdnRegistAndViewExcelVO { - @ExcelColumn(headerName = "단속년도") - private String crdnYr; + // ==================== 엑셀 샘플 순서대로 필드 정렬 ==================== - @ExcelColumn(headerName = "단속번호") - private String crdnNo; + /** 관리번호 (단속연도-단속번호) */ + @ExcelColumn(headerName = "관리번호", headerWidth = 15) + private String mngNo; - @ExcelColumn(headerName = "법정동") - private String stdgEmdCdNm; + /** 세움터 관리번호 (공백 처리) */ + @ExcelColumn(headerName = "세움터 관리번호", headerWidth = 20) + private String sewmtrMngNo; - @ExcelColumn(headerName = "지역구분") - private String rgnSeCdNm; + /** 적발일자 */ + @ExcelColumn(headerName = "적발일자", headerWidth = 15) + private String dsclYmd; - @ExcelColumn(headerName = "적발방법") - private String dsclMthdCdNm; + /** 행위일자 (현재 DB에 없음 - 필요시 추가) */ + @ExcelColumn(headerName = "행위일자", headerWidth = 15) + private String actYmd; - @ExcelColumn(headerName = "적발일자") - private String dsclYmd; + /** 최초 시정명령 일자 */ + @ExcelColumn(headerName = "최초 시정명령", headerWidth = 15) + private String crcCmdBgngYmd; - @ExcelColumn(headerName = "조사원") - private String exmnr; + /** 최근 부과일 */ + @ExcelColumn(headerName = "최근 부과일", headerWidth = 15) + private String levyBgngYmd; - @ExcelColumn(headerName = "재부과여부") - private String relevyYn; + /** 부과금액 */ + @ExcelColumn(headerName = "부과금액", headerWidth = 15) + private Long levyAmt; - @ExcelColumn(headerName = "가중부과대상") - private String agrvtnLevyTrgtYn; + /** 납부일자 (공백 처리) */ + @ExcelColumn(headerName = "납부일자", headerWidth = 15) + private String payYmd; - @ExcelColumn(headerName = "진행단계") - private String crdnPrcsSttsCdNm; + /** 번지 (지번 전체 주소) */ + @ExcelColumn(headerName = "번지", headerWidth = 40) + private String lotnoWholAddr; - @ExcelColumn(headerName = "조치처리상태") - private String actCmpltCd; + /** 상세주소 (현재 DB에 없음 - 필요시 추가) */ + @ExcelColumn(headerName = "상세주소", headerWidth = 30) + private String dtlAddr; - @ExcelColumn(headerName = "위치") - private String lotnoWholAddr; + /** 소유자(건축주) */ + @ExcelColumn(headerName = "소유자(건축주)", headerWidth = 20) + private String ownrNams; - @ExcelColumn(headerName = "행위유형") - private String actTypeCdNm; + /** 행위자(상호) */ + @ExcelColumn(headerName = "행위자(상호)", headerWidth = 20) + private String actrNams; - @ExcelColumn(headerName = "용도") - private String usgIdxCdNm; + /** 가중부과 대상 여부 */ + @ExcelColumn(headerName = "가중부과 대상", headerWidth = 15) + private String agrvtnLevyTrgtYn; - @ExcelColumn(headerName = "소유자") - private String ownrNams; + /** 불법행위 (행위 유형) */ + @ExcelColumn(headerName = "불법행위", headerWidth = 25) + private String actTypeCdNm; - @ExcelColumn(headerName = "행위자") - private String actrNams; + /** 세부내용 (현재 DB에 없음 - 필요시 추가) */ + @ExcelColumn(headerName = "세부내용", headerWidth = 25) + private String actDtlCn; - @ExcelColumn(headerName = "처분사전 일자") - private String dspsBfhdBgngYmd; + /** 용도 */ + @ExcelColumn(headerName = "용도", headerWidth = 20) + private String usgIdxCdNm; - @ExcelColumn(headerName = "시정명령 일자") - private String crcCmdBgngYmd; + /** 세부용도 (현재 DB에 없음 - 필요시 추가) */ + @ExcelColumn(headerName = "세부용도", headerWidth = 20) + private String usgDtlCn; - @ExcelColumn(headerName = "시정촉구 일자") - private String crcUrgBgngYmd; + /** 불법면적 (현재 DB에 없음 - 필요시 추가) */ + @ExcelColumn(headerName = "불법면적", headerWidth = 15) + private String illegalArea; - @ExcelColumn(headerName = "부과예고 일자") - private String levyPrvntcBgngYmd; + /** 구조 (현재 DB에 없음 - 필요시 추가) */ + @ExcelColumn(headerName = "구조", headerWidth = 20) + private String strctNm; - @ExcelColumn(headerName = "부과 일자") - private String levyBgngYmd; + /** 남은일 (현재 DB에 없음 - 필요시 추가) */ + @ExcelColumn(headerName = "남은일", headerWidth = 10) + private String remainDays; - @ExcelColumn(headerName = "납부촉구 일자") - private String payUrgBgngYmd; + /** 처분내용 (진행단계) */ + @ExcelColumn(headerName = "처분내용", headerWidth = 15) + private String crdnPrcsSttsCdNm; - @ExcelColumn(headerName = "등록일시") - private String regDt; + /** 처분일 */ + @ExcelColumn(headerName = "처분일", headerWidth = 15) + private String crdnPrcsYmd; - @ExcelColumn(headerName = "등록자") - private String rgtrNm; + /** 향후절차 (다음 진행코드명) */ + @ExcelColumn(headerName = "향후절차", headerWidth = 15) + private String nextPrcsNm; + + /** 기한 (현재 DB에 없음 - 필요시 추가) */ + @ExcelColumn(headerName = "기한", headerWidth = 15) + private String deadline; + + /** 특이사항 (비고) */ + @ExcelColumn(headerName = "특이사항", headerWidth = 40) + private String rmrk; + + /** 연락처 (현재 DB에 없음 - 필요시 추가) */ + @ExcelColumn(headerName = "연락처", headerWidth = 20) + private String telno; + + // ==================== 엑셀에 표시 안하는 추가 정보 (헤더 없음) ==================== + + /** 단속 연도 (관리번호 조합용) */ + private String crdnYr; + + /** 단속 번호 (관리번호 조합용) */ + private String crdnNo; + + /** 법정동 읍면동 코드명 */ + private String stdgEmdCdNm; + + /** 지역 구분 코드명 */ + private String rgnSeCdNm; + + /** 적발 방법 코드명 */ + private String dsclMthdCdNm; + + /** 조사원 */ + private String exmnr; + + /** 재부과 여부 */ + private String relevyYn; + + /** 조치처리상태 코드 */ + private String actCmpltCd; + + /** 우편번호 */ + private String zip; + + /** 등록일시 */ + private String regDt; + + /** 등록자명 */ + private String rgtrNm; + + /** 처분사전 시작일 */ + private String dspsBfhdBgngYmd; + + /** 처분사전 종료일 */ + private String dspsBfhdEndYmd; + + /** 시정명령 종료일 */ + private String crcCmdEndYmd; + + /** 시정촉구 시작일 */ + private String crcUrgBgngYmd; + + /** 시정촉구 종료일 */ + private String crcUrgEndYmd; + + /** 부과예고 시작일 */ + private String levyPrvntcBgngYmd; + + /** 부과예고 종료일 */ + private String levyPrvntcEndYmd; + + /** 부과 종료일 */ + private String levyEndYmd; + + /** 납부촉구 시작일 */ + private String payUrgBgngYmd; + + /** 납부촉구 종료일 */ + private String payUrgEndYmd; } diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/CrdnRegistAndViewService.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/CrdnRegistAndViewService.java index d033332..4f10d90 100644 --- a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/CrdnRegistAndViewService.java +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/CrdnRegistAndViewService.java @@ -1,5 +1,6 @@ package go.kr.project.crdn.crndRegistAndView.main.service; +import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewExcelVO; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewVO; import java.util.List; @@ -90,4 +91,13 @@ public interface CrdnRegistAndViewService { */ String selectActCmpltCd(CrdnRegistAndViewVO vo); + /** + * 엑셀 다운로드용 단속 목록을 조회합니다. + * 중요로직: 현재는 selectList와 동일한 쿼리를 사용하며, 추후 Excel 전용 최적화 가능 + * + * @param vo 검색 조건을 담은 VO 객체 (pagingYn은 "N"으로 설정하여 전체 조회) + * @return 단속 목록 (엑셀 다운로드용) + */ + List selectListForExcel(CrdnRegistAndViewVO vo); + } \ No newline at end of file diff --git a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/impl/CrdnRegistAndViewServiceImpl.java b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/impl/CrdnRegistAndViewServiceImpl.java index 4aa7bcb..8684717 100644 --- a/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/impl/CrdnRegistAndViewServiceImpl.java +++ b/src/main/java/go/kr/project/crdn/crndRegistAndView/main/service/impl/CrdnRegistAndViewServiceImpl.java @@ -5,6 +5,7 @@ import egovframework.exception.MessageException; import egovframework.util.SessionUtil; import egovframework.util.StringUtil; import go.kr.project.crdn.crndRegistAndView.main.mapper.CrdnRegistAndViewMapper; +import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewExcelVO; import go.kr.project.crdn.crndRegistAndView.main.model.CrdnRegistAndViewVO; import go.kr.project.crdn.crndRegistAndView.main.service.CrdnRegistAndViewService; import lombok.RequiredArgsConstructor; @@ -286,4 +287,21 @@ public class CrdnRegistAndViewServiceImpl extends EgovAbstractServiceImpl implem log.debug("조치처리상태 코드 조회 완료 - 조치처리상태: {}", actCmpltCd); return actCmpltCd; } + + /** + * 엑셀 다운로드용 단속 목록을 조회합니다. + * 중요로직: Excel 전용 쿼리 사용 - 부과금액(LEVY_AMT), 향후절차(NEXT_PRCS_NM) 등 추가 컬럼 포함 + * + * @param vo 검색 조건을 담은 VO 객체 (pagingYn은 "N"으로 설정하여 전체 조회) + * @return 단속 목록 (엑셀 다운로드용) + */ + @Override + public List selectListForExcel(CrdnRegistAndViewVO vo) { + log.debug("엑셀 다운로드용 단속 목록 조회 - 검색조건: {}", vo); + + // Excel 전용 쿼리 사용 (부과금액, 향후절차 등 추가 컬럼 포함) + List list = mapper.selectListForExcel(vo); + log.debug("엑셀 다운로드용 단속 목록 조회 완료 - 조회 건수: {}", list.size()); + return list; + } } \ No newline at end of file diff --git a/src/main/resources/mybatis/mapper/crdn/crndRegistAndView/main/CrdnRegistAndViewMapper_maria.xml b/src/main/resources/mybatis/mapper/crdn/crndRegistAndView/main/CrdnRegistAndViewMapper_maria.xml index 166327b..f21d345 100644 --- a/src/main/resources/mybatis/mapper/crdn/crndRegistAndView/main/CrdnRegistAndViewMapper_maria.xml +++ b/src/main/resources/mybatis/mapper/crdn/crndRegistAndView/main/CrdnRegistAndViewMapper_maria.xml @@ -501,4 +501,171 @@ AND DEL_YN = 'N' + + + \ No newline at end of file