엑셀 다운로드, 제목 넣을 수 있도록 메소드 추가, 리팩토링

다운로드용도 VO 기초 틀
실제 쿼리 및 데이터는 수정 예정
dev
박성영 2 months ago
parent b06c27bfdf
commit 26631eca43

@ -16,25 +16,115 @@ import java.util.List;
import static egovframework.util.excel.SuperClassReflectionUtils.getAllFieldsWithExcelColumn;
/**
* SXSSF(Streaming)
*
* <p> Apache POI SXSSF(Streaming) API .
* SXSSF flush .</p>
*
* <p><b> :</b></p>
* <ul>
* <li> ( )</li>
* <li> , , </li>
* <li> </li>
* <li> (AES Agile )</li>
* </ul>
*
* @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;
// ==================== 생성자 ====================
/**
*
* <p>ROW_ACCESS_WINDOW_SIZE SXSSF .</p>
*/
public BaseSxssfExcelFile() {
this.workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE);
}
// ==================== 렌더링 메서드 ====================
/**
* , , .
*
* <p> :</p>
* <ol>
* <li> ( )</li>
* <li> </li>
* <li> </li>
* <li> </li>
* </ol>
*
* @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);
}
/**
* .
* .
*
* <p> , .
* 22 , , 33 .</p>
*
* @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;
}
/**
* .
*
* <p> {@link ExcelColumn} .
* , .</p>
*
* <p> SXSSF autoSizeColumn .</p>
*
* @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());
}
/**
* .
*
* <p> {@link ExcelColumn}
* .</p>
*
* <p> :
* <ul>
* <li>headerWidth : </li>
* <li>headerWidth 0 : ( + )</li>
* </ul>
* </p>
*
* @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<Field> 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<Field> 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 호출됨)
/**
* .
*
* <p>headerWidth ,
* .</p>
*
* @param fields ExcelColumn
* @param metadata ( )
*/
private void adjustColumnWidths(List<Field> 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);
}
/**
* .
*
* <p>POI autoSizeColumn ,
* 1.3 / .</p>
*
* @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);
}
}
/**
* .
*
* <p>AES Agile .
* password null .</p>
*
* @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);
}
}
}

@ -9,42 +9,119 @@ import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public interface ExcelFile { // (1)
/**
*
*
* <p> .
* , , .</p>
*
* <p><b> :</b></p>
* <ul>
* <li> </li>
* <li> (, )</li>
* <li> (/)</li>
* </ul>
*
* @see BaseSxssfExcelFile
* @author eGovFrame
*/
public interface ExcelFile {
// ==================== 파일 출력 메서드 ====================
/**
* .
*
* @param stream
* @throws IOException
*/
void write(OutputStream stream) throws IOException;
/**
* .
*
* <p>password null .</p>
*
* @param stream
* @param password (null )
* @throws IOException
*/
void writeWithEncryption(OutputStream stream, String password) throws IOException;
// ==================== 셀 생성 메서드 ====================
/**
* .
*
* <p> :
* <ul>
* <li> (Integer, Long, Double, Float): </li>
* <li> (Boolean): </li>
* <li>/ (LocalDateTime, LocalDate, LocalTime): </li>
* <li> : toString() </li>
* </ul>
* </p>
*
* <p><b>/ :</b></p>
* <ul>
* <li>LocalDateTime: "yyyy-MM-dd HH:mm:ss"</li>
* <li>LocalDate: "yyyy-MM-dd"</li>
* <li>LocalTime: "HH:mm:ss"</li>
* </ul>
*
* @param <T>
* @param row
* @param column (0 )
* @param value (null )
* @param style
*/
default <T> 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);
}
// ==================== 스타일 생성 메서드 ====================
/**
* .
*
* <p> , .</p>
*
* @param wb
* @param isBold (true: , false: )
* @return
*/
default CellStyle createCellStyle(Workbook wb, boolean isBold) {
CellStyle style = wb.createCellStyle();
Font font = wb.createFont();

@ -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)
/**
* ()
*
* <p> VO {@link ExcelColumn} {@link ExcelSheet}
* .</p>
*
* <p><b> :</b></p>
* <ul>
* <li> VO </li>
* <li>ExcelColumn </li>
* <li>ExcelSheet </li>
* <li> </li>
* </ul>
*
* <p><b> :</b></p>
* <pre>{@code
* ExcelMetadata metadata = ExcelMetadataFactory.getInstance()
* .createMetadata(UserVO.class);
*
* String sheetName = metadata.getSheetName();
* List<String> fieldNames = metadata.getDataFieldNames();
* }</pre>
*
* @see ExcelMetadata
* @see ExcelColumn
* @see ExcelSheet
* @author eGovFrame
*/
public class ExcelMetadataFactory {
// ==================== 싱글톤 구현 ====================
/**
* private ( )
*/
private ExcelMetadataFactory() {
}
/**
*
* <p>Bill Pugh Singleton (Thread-safe, Lazy Loading)</p>
*/
private static class SingletonHolder {
private static final ExcelMetadataFactory INSTANCE = new ExcelMetadataFactory();
}
/**
* .
*
* @return ExcelMetadataFactory
*/
public static ExcelMetadataFactory getInstance() {
return SingletonHolder.INSTANCE;
}
// ==================== 메타데이터 생성 ====================
/**
* VO .
*
* <p> VO {@link ExcelColumn}
* , .</p>
*
* <p><b> :</b></p>
* <ol>
* <li> </li>
* <li>ExcelColumn </li>
* <li>, , </li>
* <li>ExcelSheet </li>
* <li>ExcelMetadata </li>
* </ol>
*
* @param clazz VO
* @return
* @throws RuntimeException ExcelColumn
*/
public ExcelMetadata createMetadata(Class<?> clazz) {
// 헤더명, 컬럼 너비, 필드명을 저장할 컬렉션 (순서 유지)
Map<String, String> headerNamesMap = new LinkedHashMap<>();
Map<String, Integer> headerWidthsMap = new LinkedHashMap<>();
List<String> 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<String, String> 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) {
/**
* .
*
* <p>{@link ExcelSheet} ,
* "Sheet1" .</p>
*
* @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";
}
}

@ -5,17 +5,164 @@ import lombok.Getter;
import java.util.List;
/**
* DTO
*
* <p> .
* , VO , () .</p>
*
* <p><b> :</b></p>
* <ul>
* <li>(Immutable) - final</li>
* <li> - of() </li>
* <li> - </li>
* </ul>
*
* <p><b> 1: </b></p>
* <pre>{@code
* // VO 리스트 조회
* List<UserVO> userList = userService.selectUserList();
*
* // ExcelSheetData 생성 (제목 없음)
* ExcelSheetData sheetData = ExcelSheetData.of(userList, UserVO.class);
*
* // 엑셀 파일 생성
* new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx");
* }</pre>
*
* <p><b> 2: </b></p>
* <pre>{@code
* // VO 리스트 조회
* List<UserVO> userList = userService.selectUserList();
*
* // ExcelSheetData 생성 (제목 포함)
* ExcelSheetData sheetData = ExcelSheetData.of(
* userList,
* UserVO.class,
* "2024년 1월 사용자 목록" // 제목 행에 표시될 텍스트
* );
*
* // 엑셀 파일 생성 (첫 행에 제목이 표시됨)
* new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx");
* }</pre>
*
* <p><b> 3: </b></p>
* <pre>{@code
* // 시트 1: 사용자 목록
* List<UserVO> userList = userService.selectUserList();
* ExcelSheetData userSheet = ExcelSheetData.of(userList, UserVO.class, "사용자 목록");
*
* // 시트 2: 부서 목록
* List<DeptVO> deptList = deptService.selectDeptList();
* ExcelSheetData deptSheet = ExcelSheetData.of(deptList, DeptVO.class, "부서 목록");
*
* // 다중 시트 그룹 생성
* ExcelSheetDataGroup dataGroup = ExcelSheetDataGroup.of(
* List.of(userSheet, deptSheet)
* );
*
* // 다중 시트 엑셀 파일 생성
* new SxssfMultiSheetExcelFile(dataGroup, response);
* }</pre>
*
* @see SxssfExcelFile
* @see SxssfMultiSheetExcelFile
* @see ExcelSheetDataGroup
* @author eGovFrame
*/
@Getter
@AllArgsConstructor
public class ExcelSheetData { // (1)
public class ExcelSheetData {
// ==================== 필드 ====================
/**
*
*
* <p> {@link ExcelColumn} VO .
* (row) .</p>
*/
private final List<?> dataList;
/**
* (VO )
*
* <p> :
* <ul>
* <li>{@link ExcelColumn} - </li>
* <li>{@link ExcelSheet} - ()</li>
* </ul>
* </p>
*
* <p><b>:</b></p>
* <pre>{@code
* @ExcelSheet(name = "사용자")
* public class UserVO {
* @ExcelColumn(headerName = "이름", headerWidth = 20)
* private String userName;
*
* @ExcelColumn(headerName = "이메일")
* private String email;
* }
* }</pre>
*/
private final Class<?> type;
private final String title; // 제목 행 텍스트 (null이면 제목 행 생성하지 않음)
/**
* ()
*
* <p> null ,
* . ,
* 22 .</p>
*
* <p>null ,
* .</p>
*/
private final String title;
// ==================== 팩토리 메서드 ====================
/**
* ExcelSheetData .
*
* <p> .</p>
*
* <p><b> :</b></p>
* <pre>
* Row 0: [1] [2] [3] ...
* Row 1: [] [] [] ...
* Row 2: [] [] [] ...
* ...
* </pre>
*
* @param dataList (null )
* @param type (VO , null )
* @return ExcelSheetData
*/
public static ExcelSheetData of(List<?> dataList, Class<?> type) {
return new ExcelSheetData(dataList, type, null);
}
/**
* ExcelSheetData .
*
* <p> , , .
* .</p>
*
* <p><b> :</b></p>
* <pre>
* Row 0: [ - ]
* Row 1: [1] [2] [3] ...
* Row 2: [] [] [] ...
* Row 3: [] [] [] ...
* ...
* </pre>
*
* @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);
}

@ -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;
/**
*
*
* <p> {@link BaseSxssfExcelFile} .
* HTTP , OutputStream .</p>
*
* <p><b> 1: HTTP </b></p>
* <pre>{@code
* @GetMapping("/download.xlsx")
* public void downloadExcel(HttpServletRequest request, HttpServletResponse response) {
* List<UserVO> dataList = userService.selectUserList();
* ExcelSheetData sheetData = ExcelSheetData.of(dataList, UserVO.class, "사용자 목록");
* new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx");
* }
* }</pre>
*
* <p><b> 2: </b></p>
* <pre>{@code
* @GetMapping("/download-encrypted.xlsx")
* public void downloadEncryptedExcel(HttpServletRequest request, HttpServletResponse response) {
* List<UserVO> dataList = userService.selectUserList();
* ExcelSheetData sheetData = ExcelSheetData.of(dataList, UserVO.class, "사용자 목록");
* new SxssfExcelFile(sheetData, request, response, "사용자목록.xlsx", "password123");
* }
* }</pre>
*
* <p><b> 3: OutputStream </b></p>
* <pre>{@code
* try (FileOutputStream fos = new FileOutputStream("output.xlsx")) {
* List<UserVO> dataList = userService.selectUserList();
* ExcelSheetData sheetData = ExcelSheetData.of(dataList, UserVO.class);
* new SxssfExcelFile(sheetData, fos, null);
* }
* }</pre>
*
* <p><b>VO :</b></p>
* <pre>{@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;
* }
* }</pre>
*
* @see BaseSxssfExcelFile
* @see ExcelSheetData
* @see ExcelColumn
* @author eGovFrame
*/
public class SxssfExcelFile extends BaseSxssfExcelFile {
// ==================== 생성자 (HTTP 응답 다운로드) ====================
/**
* HTTP ( ).
*
* <p> HTTP .
* .</p>
*
* @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 ( ).
*
* <p> HTTP .
* .</p>
*
* @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 .
*
* <p> OutputStream .
* .</p>
*
* @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 .
*
* <p> Content-Disposition :
* <ul>
* <li>IE (MSIE/Trident): UTF-8 URL (+ )</li>
* <li> : UTF-8 ISO-8859-1 </li>
* </ul>
* </p>
*
* @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"));
}
}

@ -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 {
/**
*
*
* <p> {@link BaseSxssfExcelFile} .
* .</p>
*
* <p><b> 1: </b></p>
* <pre>{@code
* @GetMapping("/download-multi.xlsx")
* public void downloadMultiSheetExcel(HttpServletResponse response) throws IOException {
* // 시트 1: 사용자 목록
* List<UserVO> userList = userService.selectUserList();
* ExcelSheetData userSheet = ExcelSheetData.of(userList, UserVO.class, "사용자 목록");
*
* // 시트 2: 부서 목록
* List<DeptVO> 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);
* }
* }</pre>
*
* <p><b> 2: </b></p>
* <pre>{@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");
* }
* }</pre>
*
* <p><b>:</b></p>
* <ul>
* <li> VO .</li>
* <li> VO {@link ExcelSheet} .</li>
* <li> .</li>
* </ul>
*
* @see BaseSxssfExcelFile
* @see ExcelSheetDataGroup
* @see ExcelSheetData
* @author eGovFrame
*/
public class SxssfMultiSheetExcelFile extends BaseSxssfExcelFile {
// ==================== 생성자 ====================
/**
* HTTP ( ).
*
* <p> HTTP .</p>
*
* @param dataGroup
* @param response HTTP
* @throws IOException
*/
public SxssfMultiSheetExcelFile(ExcelSheetDataGroup dataGroup, HttpServletResponse response)
throws IOException {
this(dataGroup, response, null);
}
/**
* HTTP ( ).
*
* <p> HTTP .</p>
*
* @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 헬퍼 메서드 ====================
/**
* .
*
* <p> .
* , , .</p>
*
* @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.
}
}
/**
* .
*
* <p> .
* renderSheetContent .</p>
*/
private void resetSheetContext() {
sheet = null;
titleRowOffset = 0;
}
}

@ -416,37 +416,7 @@ public class CrdnRegistAndViewController {
paramVO.setPagingYn("N");
// 단속 목록 조회
List<CrdnRegistAndViewVO> list = service.selectList(paramVO);
// VO를 ExcelVO로 변환
List<CrdnRegistAndViewExcelVO> 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<CrdnRegistAndViewExcelVO> excelList = service.selectListForExcel(paramVO);
// 엑셀 파일 생성 및 다운로드 (제목 행 포함)
String filename = "단속목록_" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";

@ -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<CrdnRegistAndViewExcelVO> selectListForExcel(CrdnRegistAndViewVO vo);
}

@ -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;
}

@ -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<CrdnRegistAndViewExcelVO> selectListForExcel(CrdnRegistAndViewVO vo);
}

@ -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<CrdnRegistAndViewExcelVO> selectListForExcel(CrdnRegistAndViewVO vo) {
log.debug("엑셀 다운로드용 단속 목록 조회 - 검색조건: {}", vo);
// Excel 전용 쿼리 사용 (부과금액, 향후절차 등 추가 컬럼 포함)
List<CrdnRegistAndViewExcelVO> list = mapper.selectListForExcel(vo);
log.debug("엑셀 다운로드용 단속 목록 조회 완료 - 조회 건수: {}", list.size());
return list;
}
}

@ -501,4 +501,171 @@
AND DEL_YN = 'N'
</update>
<!-- 엑셀 다운로드용 단속 목록 조회 -->
<select id="selectListForExcel" parameterType="CrdnRegistAndViewVO" resultType="CrdnRegistAndViewExcelVO">
/* CrdnRegistAndViewMapper.selectListForExcel : 엑셀 다운로드용 단속 목록 조회 */
/* 중요로직: 기존 selectList와 동일하되, 부과금액(tb_levy_info.IMPLT_CPSR_AMT)과 향후절차(다음 진행코드) 추가 */
SELECT m.*
FROM (
SELECT
c.CRDN_YR, /* 단속 연도 */
c.CRDN_NO, /* 단속 번호 */
c.SGG_CD, /* 시군구 코드 */
sgg.CD_NM AS SGG_CD_NM,
c.RGN_SE_CD, /* 지역 구분 코드 */
rgn.CD_NM AS RGN_SE_CD_NM,
c.DSCL_MTHD_CD, /* 단속 방법 코드 */
dscl.CD_NM AS DSCL_MTHD_CD_NM,
c.DSCL_YMD, /* 적발 일자 */
c.EXMNR, /* 조사원 */
c.RMRK, /* 비고 (특이사항) */
c.DSPS_BFHD_BGNG_YMD, /* 사전처분 시작일 */
c.DSPS_BFHD_END_YMD, /* 사전처분 종료일 */
c.CRC_CMD_BGNG_YMD, /* 시정명령 시작일 */
c.CRC_CMD_END_YMD, /* 시정명령 종료일 */
c.CRC_URG_BGNG_YMD, /* 시정촉구 시작일 */
c.CRC_URG_END_YMD, /* 시정촉구 종료일 */
c.LEVY_PRVNTC_BGNG_YMD, /* 부과예고 시작일 */
c.LEVY_PRVNTC_END_YMD, /* 부과예고 종료일 */
c.LEVY_BGNG_YMD, /* 부과 시작일 */
c.LEVY_END_YMD, /* 부과 종료일 */
c.PAY_URG_BGNG_YMD, /* 납부촉구 시작일 */
c.PAY_URG_END_YMD, /* 납부촉구 종료일 */
c.FRST_CRDN_YR, /* 최초 단속 연도 */
c.FRST_CRDN_NO, /* 최초 단속 번호 */
c.RELEVY_YN, /* 재과 여부 */
c.AGRVTN_LEVY_TRGT_YN, /* 가중 부과 대상 여부 */
c.CRDN_PRCS_STTS_CD, /* 단속 처리 상태 코드 */
stts.CD_NM AS CRDN_PRCS_STTS_CD_NM,
c.CRDN_PRCS_YMD, /* 단속 처리 일자 */
c.REG_DT,
c.RGTR,
u.USER_ACNT AS RGTR_ACNT,
u.USER_NM AS RGTR_NM,
p.LOTNO_WHOL_ADDR, /* 지번 전체 주소 */
p.STDG_EMD_CD, /* 법정동 읍면동 코드 */
emd.CD_NM AS STDG_EMD_CD_NM,
p.ZIP,
(SELECT GROUP_CONCAT(DISTINCT o2.FLNM SEPARATOR ', ')
FROM tb_ownr_info oi2
LEFT JOIN tb_ownr o2 ON o2.OWNR_ID = oi2.OWNR_ID AND o2.DEL_YN = 'N'
WHERE oi2.CRDN_YR = c.CRDN_YR
AND oi2.CRDN_NO = c.CRDN_NO
AND oi2.DEL_YN = 'N') AS OWNR_NAMS,
(SELECT GROUP_CONCAT(DISTINCT o2.FLNM SEPARATOR ', ')
FROM tb_actr_info ai
LEFT JOIN tb_ownr o2 ON o2.OWNR_ID = ai.OWNR_ID AND o2.DEL_YN = 'N'
WHERE ai.CRDN_YR = c.CRDN_YR
AND ai.CRDN_NO = c.CRDN_NO
AND ai.DEL_YN = 'N') AS ACTR_NAMS,
a.ACT_TYPE_CD, /* 행위 유형 코드 */
CASE WHEN IFNULL(act_cnt.ACT_ALL_CNT, 0) > 1 THEN
CONCAT(act.VLTN_BDST, ' 등 ', act_cnt.ACT_ALL_CNT, '건')
ELSE act.VLTN_BDST END ACT_TYPE_CD_NM,
IFNULL(act_cnt.ACT_ALL_CNT, 0) as ACT_ALL_CNT,
IFNULL(act_cnt.ACT_COMPLT_CNT, 0) as ACT_COMPLT_CNT,
/* 중요로직: ACT_CMPLT_CD를 메인 SELECT에서 직접 계산 - 외부 SELECT에서 재계산 불필요 */
case when IFNULL(act_cnt.ACT_ALL_CNT, 0) = 0 then '0'
when IFNULL(act_cnt.ACT_ALL_CNT, 0) != 0 and IFNULL(act_cnt.ACT_ALL_CNT, 0) != IFNULL(act_cnt.ACT_COMPLT_CNT, 0) then '1'
when IFNULL(act_cnt.ACT_ALL_CNT, 0) != 0 and IFNULL(act_cnt.ACT_ALL_CNT, 0) = IFNULL(act_cnt.ACT_COMPLT_CNT, 0) then '3'
else '-'
end as ACT_CMPLT_CD, /* [행위정보없음 0: , 미조치 : 1, 조치완료 : 3] */
a.USG_IDX_CD, /* 용도 지수 코드 */
usg.USG_NM AS USG_IDX_CD_NM,
/* 엑셀 다운로드 전용 컬럼 */
CONCAT(c.CRDN_YR, '-', c.CRDN_NO) AS MNG_NO, /* 관리번호 (단속연도-단속번호) */
'' AS SEWMTR_MNG_NO, /* 세움터 관리번호 (공백 처리) */
'' AS ACT_YMD, /* 행위일자 (현재 DB에 없음) */
(SELECT li.IMPLT_CPSR_AMT
FROM tb_levy_info li
WHERE li.CRDN_YR = c.CRDN_YR
AND li.CRDN_NO = c.CRDN_NO
AND li.DEL_YN = 'N'
ORDER BY li.LEVY_INFO_ID
LIMIT 1) AS LEVY_AMT, /* 부과금액 (첫번째 값) */
'' AS PAY_YMD, /* 납부일자 (공백 처리) */
(SELECT next_cd.CD_NM
FROM tb_cd_detail curr_cd
LEFT JOIN tb_cd_detail next_cd
ON next_cd.CD_GROUP_ID = 'CRDN_PRCS_STTS_CD'
AND next_cd.USE_YN = 'Y'
AND next_cd.SORT_ORDR = (
SELECT MIN(ncd.SORT_ORDR)
FROM tb_cd_detail ncd
WHERE ncd.CD_GROUP_ID = 'CRDN_PRCS_STTS_CD'
AND ncd.USE_YN = 'Y'
AND ncd.SORT_ORDR > curr_cd.SORT_ORDR
)
WHERE curr_cd.CD_GROUP_ID = 'CRDN_PRCS_STTS_CD'
AND curr_cd.CD_ID = c.CRDN_PRCS_STTS_CD) AS NEXT_PRCS_NM /* 향후절차 (다음 진행코드명) */
FROM tb_crdn c
LEFT JOIN tb_cd_detail sgg ON sgg.CD_GROUP_ID = 'ORG_CD' AND sgg.CD_ID = c.SGG_CD
LEFT JOIN tb_cd_detail rgn ON rgn.CD_GROUP_ID = 'RGN_SE_CD' AND rgn.CD_ID = c.RGN_SE_CD
LEFT JOIN tb_cd_detail dscl ON dscl.CD_GROUP_ID = 'DSCL_MTHD_CD' AND dscl.CD_ID = c.DSCL_MTHD_CD
LEFT JOIN tb_cd_detail stts ON stts.CD_GROUP_ID = 'CRDN_PRCS_STTS_CD' AND stts.CD_ID = c.CRDN_PRCS_STTS_CD
LEFT JOIN tb_user u ON u.USER_ID = c.RGTR AND u.USE_YN = 'Y'
LEFT JOIN tb_pstn_info p ON p.CRDN_YR = c.CRDN_YR AND p.CRDN_NO = c.CRDN_NO AND p.DEL_YN = 'N'
LEFT JOIN tb_cd_detail emd ON emd.CD_GROUP_ID = 'STDG_EMD_CD' AND emd.CD_ID = p.STDG_EMD_CD
LEFT JOIN (
SELECT
CRDN_YR,
CRDN_NO,
COUNT(1) as ACT_ALL_CNT,
SUM(CASE WHEN ACTN_PRCS_STTS_CD = '3' THEN 1 ELSE 0 END) as ACT_COMPLT_CNT
FROM tb_act_info
WHERE DEL_YN = 'N'
GROUP BY CRDN_YR, CRDN_NO
) act_cnt ON act_cnt.CRDN_YR = c.CRDN_YR AND act_cnt.CRDN_NO = c.CRDN_NO
LEFT Join tb_act_info a ON a.CRDN_YR = c.CRDN_YR and a.CRDN_NO = c.CRDN_NO AND a.DEL_YN = 'N' AND a.ACT_NO = (SELECT MIN(a1.ACT_NO) FROM tb_act_info a1 WHERE a1.CRDN_YR = a.CRDN_YR AND a1.CRDN_NO = a.CRDN_NO AND a1.DEL_YN='N')
LEFT JOIN tb_act_type act ON act.ACT_TYPE_CD = a.ACT_TYPE_CD
LEFT JOIN tb_usg_idx usg ON usg.USG_IDX_CD = a.USG_IDX_CD AND usg.DEL_YN = 'N'
WHERE c.DEL_YN = 'N'
<if test='schCrdnYr != null and schCrdnYr != ""'>
AND c.CRDN_YR = #{schCrdnYr}
</if>
<if test='schCrdnNo != null and schCrdnNo != ""'>
AND c.CRDN_NO LIKE CONCAT('%', #{schCrdnNo}, '%')
</if>
<if test='schStdgEmdCd != null and schStdgEmdCd != ""'>
AND p.STDG_EMD_CD = #{schStdgEmdCd}
</if>
<if test='schRgnSeCd != null and schRgnSeCd != ""'>
AND c.RGN_SE_CD = #{schRgnSeCd}
</if>
<if test='schDsclMthdCd != null and schDsclMthdCd != ""'>
AND c.DSCL_MTHD_CD = #{schDsclMthdCd}
</if>
<if test='schExmnr != null and schExmnr != ""'>
AND c.EXMNR LIKE CONCAT('%', #{schExmnr}, '%')
</if>
<if test='schCrdnPrcsSttsCd != null and schCrdnPrcsSttsCd != ""'>
AND c.CRDN_PRCS_STTS_CD = #{schCrdnPrcsSttsCd}
</if>
<if test='schAgrvtnLevyTrgtYn != null and schAgrvtnLevyTrgtYn != ""'>
AND c.AGRVTN_LEVY_TRGT_YN = #{schAgrvtnLevyTrgtYn}
</if>
<if test='schZip != null and schZip != ""'>
AND p.ZIP LIKE CONCAT('%', #{schZip}, '%')
</if>
<if test='schLotnoAddr != null and schLotnoAddr != ""'>
AND p.LOTNO_ADDR LIKE CONCAT('%', #{schLotnoAddr}, '%')
</if>
<if test='schDtlAddr != null and schDtlAddr != ""'>
AND p.DTL_ADDR LIKE CONCAT('%', #{schDtlAddr}, '%')
</if>
<if test='schLotnoMno != null and schLotnoMno != ""'>
AND p.LOTNO_MNO = #{schLotnoMno}
</if>
<if test='schLotnoSno != null and schLotnoSno != ""'>
AND p.LOTNO_SNO = #{schLotnoSno}
</if>
) m
WHERE 1=1
<if test='schActCmpltCd != null and schActCmpltCd != ""'>
/* 중요로직: ACT_CMPLT_CD가 메인 SELECT에서 이미 계산되어 단순 비교만 수행 */
AND m.ACT_CMPLT_CD = #{schActCmpltCd} /* [행위정보없음 0: , 미조치 : 1, 조치완료 : 3] */
</if>
ORDER BY m.CRDN_YR DESC, m.CRDN_NO DESC
</select>
</mapper>
Loading…
Cancel
Save