엑셀다운로드 UTIL 로직 및 수식, 서식추가 등

대상쿼리 수정
dev
박성영 2 months ago
parent a38498eb55
commit f6a508f10f

@ -13,6 +13,7 @@ import java.io.OutputStream;
import java.lang.reflect.Field;
import java.security.GeneralSecurityException;
import java.util.List;
import java.math.BigDecimal;
import static egovframework.util.excel.SuperClassReflectionUtils.getAllFieldsWithExcelColumn;
@ -208,6 +209,17 @@ public abstract class BaseSxssfExcelFile implements ExcelFile {
protected void renderDataLines(ExcelSheetData data, ExcelMetadata metadata) {
CellStyle dataStyle = createCellStyle(workbook, false);
// 숫자 서식 스타일 생성(정수/소수) - 기본 스타일을 복제하여 데이터 포맷만 부여
CellStyle integerNumberStyle = workbook.createCellStyle();
integerNumberStyle.cloneStyleFrom(dataStyle);
short intDf = workbook.createDataFormat().getFormat("#,##0");
integerNumberStyle.setDataFormat(intDf);
CellStyle decimalNumberStyle = workbook.createCellStyle();
decimalNumberStyle.cloneStyleFrom(dataStyle);
short decDf = workbook.createDataFormat().getFormat("#,##0.##");
decimalNumberStyle.setDataFormat(decDf);
// 데이터 시작 행 (제목이 있으면 row 2, 없으면 row 1)
int rowIndex = ROW_START_INDEX + titleRowOffset + 1;
List<Field> fields = getAllFieldsWithExcelColumn(data.getType());
@ -215,7 +227,7 @@ public abstract class BaseSxssfExcelFile implements ExcelFile {
// 각 데이터 객체를 행으로 변환
for (Object record : data.getDataList()) {
Row row = sheet.createRow(rowIndex++);
renderDataRow(row, record, fields, dataStyle);
renderDataRow(row, record, fields, dataStyle, integerNumberStyle, decimalNumberStyle);
}
// 컬럼 너비 조정
@ -239,6 +251,20 @@ public abstract class BaseSxssfExcelFile implements ExcelFile {
titleFont.setBold(true);
titleStyle.setFont(titleFont);
// 중요 로직(한글): 제목 셀에도 실선(THIN) 보더를 적용하여 표의 일관성 유지
titleStyle.setBorderTop(BorderStyle.THIN);
titleStyle.setBorderBottom(BorderStyle.THIN);
titleStyle.setBorderLeft(BorderStyle.THIN);
titleStyle.setBorderRight(BorderStyle.THIN);
// 중요 로직(한글): 제목 배경색 지정 - 요구사항에 따라 #be8e00 색상을 적용
// SXSSF에서는 내부적으로 XSSFCellStyle을 사용하므로 캐스팅하여 사용자 색상 설정
if (titleStyle instanceof org.apache.poi.xssf.usermodel.XSSFCellStyle) {
org.apache.poi.xssf.usermodel.XSSFCellStyle xssf = (org.apache.poi.xssf.usermodel.XSSFCellStyle) titleStyle;
xssf.setFillForegroundColor(new org.apache.poi.xssf.usermodel.XSSFColor(new java.awt.Color(0xBE, 0x8E, 0x00), null));
xssf.setFillPattern(FillPatternType.SOLID_FOREGROUND);
}
return titleStyle;
}
@ -250,6 +276,14 @@ public abstract class BaseSxssfExcelFile implements ExcelFile {
private CellStyle createHeaderCellStyle() {
CellStyle headerStyle = createCellStyle(workbook, true);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
// 중요 로직(한글): 헤더 배경색 지정 - 요구사항에 따라 #fde598 색상을 적용
if (headerStyle instanceof org.apache.poi.xssf.usermodel.XSSFCellStyle) {
org.apache.poi.xssf.usermodel.XSSFCellStyle xssf = (org.apache.poi.xssf.usermodel.XSSFCellStyle) headerStyle;
xssf.setFillForegroundColor(new org.apache.poi.xssf.usermodel.XSSFColor(new java.awt.Color(0xFD, 0xE5, 0x98), null));
xssf.setFillPattern(FillPatternType.SOLID_FOREGROUND);
}
return headerStyle;
}
@ -271,21 +305,90 @@ public abstract class BaseSxssfExcelFile implements ExcelFile {
* @param row
* @param record
* @param fields ExcelColumn
* @param style
* @param baseStyle ( )
* @param integerNumberStyle (#,##0)
* @param decimalNumberStyle (#,##0.##)
* @throws RuntimeException
*/
private void renderDataRow(Row row, Object record, List<Field> fields, CellStyle style) {
private void renderDataRow(Row row, Object record, List<Field> fields, CellStyle baseStyle, CellStyle integerNumberStyle, CellStyle decimalNumberStyle) {
int columnIndex = COLUMN_START_INDEX;
try {
for (Field field : fields) {
field.setAccessible(true);
createCell(row, columnIndex++, field.get(record), style);
Object value = field.get(record);
// 수식 처리: ExcelColumn에 formula 설정이 있는 경우 수식을 생성하여 설정
ExcelColumn excelColumn = field.getAnnotation(ExcelColumn.class);
if (excelColumn != null && excelColumn.formula() && !excelColumn.formulaRefField().isEmpty()) {
String refFieldName = excelColumn.formulaRefField();
int refColumnIndex = findFieldColumnIndex(fields, refFieldName);
if (refColumnIndex >= 0) {
// 한글 중요 주석: 참조 대상(X열) 값이 null 또는 공백("")이면 수식을 적용하지 않고 빈 셀로 처리하여 컬럼 정렬 유지
Field refField = fields.get(refColumnIndex);
refField.setAccessible(true);
Object refValue = refField.get(record);
boolean isBlankRef = (refValue == null) || (refValue instanceof String && ((String) refValue).trim().isEmpty());
if (isBlankRef) {
Cell cell = row.createCell(columnIndex++);
cell.setCellValue("");
cell.setCellStyle(baseStyle);
continue; // 다음 필드 처리
}
String refExcelColumn = toExcelColumnName(refColumnIndex + 1); // 1-based for Excel column letters
int excelRowNumber = row.getRowNum() + 1; // 1-based row number in Excel
String formula = String.format(excelColumn.formulaPattern(), refExcelColumn, excelRowNumber);
Cell cell = row.createCell(columnIndex++);
cell.setCellFormula(formula);
cell.setCellStyle(baseStyle);
continue; // 다음 필드 처리
}
}
// 기본 값 처리: 숫자 타입일 경우 천단위 콤마 서식 적용
CellStyle styleToUse = baseStyle;
if (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long) {
styleToUse = integerNumberStyle;
} else if (value instanceof Float || value instanceof Double || value instanceof BigDecimal) {
styleToUse = decimalNumberStyle;
}
createCell(row, columnIndex++, value, styleToUse);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("데이터 필드 접근 중 오류가 발생했습니다.", e);
}
}
/**
* .
* : ExcelColumn 0 .
*/
private int findFieldColumnIndex(List<Field> fields, String targetFieldName) {
for (int i = 0; i < fields.size(); i++) {
if (fields.get(i).getName().equals(targetFieldName)) {
return i;
}
}
return -1;
}
/**
* 1 (A, B, ... AA, AB ...) .
* : .
*/
private String toExcelColumnName(int columnNumber) {
StringBuilder sb = new StringBuilder();
int num = columnNumber;
while (num > 0) {
int rem = (num - 1) % 26;
sb.insert(0, (char) ('A' + rem));
num = (num - 1) / 26;
}
return sb.toString();
}
/**
* .
*

@ -10,4 +10,23 @@ import java.lang.annotation.Target;
public @interface ExcelColumn {
String headerName() default "";
int headerWidth() default 0; // 0이면 자동 조정, 0보다 크면 지정된 너비 사용
// ==================== 수식 지원 옵션 ====================
/**
*
* : true , .
*/
boolean formula() default false;
/**
*
* : ) "deadline" , .
*/
String formulaRefField() default "";
/**
* . %s (A, B, ...), %d (1) .
* : "=%s%d-TODAY()" , "참조셀 - 오늘" .
*/
String formulaPattern() default "=%s%d-TODAY()";
}

@ -76,13 +76,14 @@ public interface ExcelFile {
* @param style
*/
default <T> void createCell(Row row, int column, T value, CellStyle style) {
// null 값은 빈 셀로 처리 (NPE 방지)
// null 값도 보더가 보이도록 빈 셀 생성 후 스타일 적용 (NPE 방지)
Cell cell = row.createCell(column);
if (value == null) {
cell.setCellValue("");
cell.setCellStyle(style);
return;
}
Cell cell = row.createCell(column);
// 타입별로 적절한 셀 값 설정
if (value instanceof Integer) {
cell.setCellValue((Integer) value);
@ -127,6 +128,14 @@ public interface ExcelFile {
Font font = wb.createFont();
font.setBold(isBold);
style.setFont(font);
// 중요 로직(한글): 생성되는 모든 셀에 대해 실선(THIN) 보더 적용
// 데이터 셀과 헤더 셀은 본 메서드를 통해 스타일이 생성되므로, 여기서 공통 보더를 지정한다.
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
return style;
}
}

@ -420,7 +420,7 @@ public class CrdnRegistAndViewController {
// 엑셀 파일 생성 및 다운로드 (제목 행 포함)
String filename = "단속목록_" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
new SxssfExcelFile(ExcelSheetData.of(excelList, CrdnRegistAndViewExcelVO.class, "단속 목록"), request, response, filename);
new SxssfExcelFile(ExcelSheetData.of(excelList, CrdnRegistAndViewExcelVO.class, "단속 목록 "+excelList.size() +"건"), request, response, filename);
log.debug("단속 목록 엑셀 다운로드 완료 - 파일명: {}, 건수: {}", filename, excelList.size());
} catch (Exception e) {

@ -60,7 +60,8 @@ public class CrdnRegistAndViewExcelVO {
private String payYmd;
/** 번지 (지번 전체 주소) */
@ExcelColumn(headerName = "번지", headerWidth = 40)
//@ExcelColumn(headerName = "번지", headerWidth = 40)
@ExcelColumn(headerName = "지번주소", headerWidth = 40)
private String lotnoWholAddr;
/** 상세주소 */
@ -103,8 +104,8 @@ public class CrdnRegistAndViewExcelVO {
@ExcelColumn(headerName = "구조", headerWidth = 20)
private String strctNm;
/** 남은일 (서식에서 today - 처분내용별 종료일자) */
@ExcelColumn(headerName = "남은일", headerWidth = 10)
/** 남은일 (서식에서 today - 처분내용별 종료일자, =X3-TODAY(), X3 은 deadline 열과 행 ) */
@ExcelColumn(headerName = "남은일", headerWidth = 10, formula = true, formulaRefField = "deadline")
private String remainDays;
/** 처분내용 (진행단계) */

@ -558,7 +558,15 @@
strct_ai.STRCT_NM, /* 구조 (구조지수명) */
'' AS REMAIN_DAYS, /* 남은일 엑셀에서 today - DEADLINE 서식으로 표현예정*/
stts.CD_NM AS CRDN_PRCS_STTS_CD_NM, /* 처분내용 (진행단계) */
c.CRDN_PRCS_YMD, /* 처분일 */
CASE
WHEN c.CRDN_PRCS_STTS_CD = '20' THEN DATE_FORMAT(STR_TO_DATE(c.DSPS_BFHD_BGNG_YMD, '%Y%m%d'), '%Y-%m-%d')
WHEN c.CRDN_PRCS_STTS_CD = '30' THEN DATE_FORMAT(STR_TO_DATE(c.CRC_CMD_BGNG_YMD, '%Y%m%d'), '%Y-%m-%d')
WHEN c.CRDN_PRCS_STTS_CD = '40' THEN DATE_FORMAT(STR_TO_DATE(c.CRC_URG_BGNG_YMD, '%Y%m%d'), '%Y-%m-%d')
WHEN c.CRDN_PRCS_STTS_CD = '50' THEN DATE_FORMAT(STR_TO_DATE(c.LEVY_PRVNTC_BGNG_YMD, '%Y%m%d'), '%Y-%m-%d')
WHEN c.CRDN_PRCS_STTS_CD = '60' THEN DATE_FORMAT(STR_TO_DATE(c.LEVY_BGNG_YMD, '%Y%m%d'), '%Y-%m-%d')
WHEN c.CRDN_PRCS_STTS_CD = '70' THEN DATE_FORMAT(STR_TO_DATE(c.PAY_URG_BGNG_YMD, '%Y%m%d'), '%Y-%m-%d')
ELSE NULL
END AS CRDN_PRCS_YMD, /* 처분일 */
(SELECT next_cd.CD_NM
FROM tb_cd_detail curr_cd
LEFT JOIN tb_cd_detail next_cd
@ -616,7 +624,8 @@
emd.CD_NM AS STDG_EMD_CD_NM,
p.ZIP,
ai.ACT_TYPE_CD, /* 행위 유형 코드 */
ai.USG_IDX_CD /* 용도 지수 코드 */
ai.USG_IDX_CD, /* 용도 지수 코드 */
ai.ACT_INFO_ID
FROM tb_crdn c
inner join tb_act_info ai on ai.CRDN_NO = c.CRDN_NO and ai.CRDN_YR = c.CRDN_YR and ai.DEL_YN = 'N'
LEFT JOIN tb_cd_detail sgg ON sgg.CD_GROUP_ID = 'ORG_CD' AND sgg.CD_ID = c.SGG_CD
@ -671,7 +680,7 @@
</if>
) m
WHERE 1=1
ORDER BY m.CRDN_YR DESC, m.CRDN_NO DESC
ORDER BY m.CRDN_YR DESC, m.CRDN_NO DESC, m.ACT_INFO_ID
</select>
</mapper>
Loading…
Cancel
Save