엑셀 다운로드, 기본 로직, 다운로드 로직에 제목 추가 로직 예정

중간 업로드
dev
박성영 2 months ago
parent b495bcc71e
commit b06c27bfdf

@ -4,9 +4,8 @@ import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionMode;
import org.apache.poi.poifs.crypt.Encryptor;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.IOException;
@ -23,25 +22,91 @@ public abstract class BaseSxssfExcelFile implements ExcelFile {
protected static final int COLUMN_START_INDEX = 0;
protected SXSSFWorkbook workbook;
protected Sheet sheet;
protected int titleRowOffset = 0; // 제목 행이 있으면 1, 없으면 0
public BaseSxssfExcelFile() {
this.workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE);
}
/**
* .
* .
*
* @param sheetName
* @param title
* @param columnCount
*/
protected void renderTitleRow(String sheetName, String title, int columnCount) {
// 시트가 없으면 생성
if (sheet == null) {
sheet = workbook.createSheet(sheetName);
}
// 제목 행 생성 (row 0)
Row titleRow = sheet.createRow(ROW_START_INDEX);
// 제목 스타일 생성 (굵게, 가운데 정렬)
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);
// 첫 번째 셀에 제목 입력
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 // 종료 열
));
}
// 제목 행 오프셋 설정
titleRowOffset = 1;
}
protected void renderHeaders(ExcelMetadata excelMetadata) {
sheet = workbook.createSheet(excelMetadata.getSheetName());
Row row = sheet.createRow(ROW_START_INDEX);
// 시트가 없으면 생성 (제목 행이 없는 경우)
if (sheet == null) {
sheet = workbook.createSheet(excelMetadata.getSheetName());
}
// 헤더 행 생성 (제목 행이 있으면 row 1, 없으면 row 0)
Row row = sheet.createRow(ROW_START_INDEX + titleRowOffset);
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);
}
// 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);
}
}
protected void renderDataLines(ExcelSheetData data) {
protected void renderDataLines(ExcelSheetData data, ExcelMetadata metadata) {
CellStyle style = createCellStyle(workbook, false);
int rowIndex = ROW_START_INDEX + 1;
// 데이터 시작 행 (제목 행이 있으면 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;
@ -54,6 +119,33 @@ public abstract class BaseSxssfExcelFile implements ExcelFile {
throw new RuntimeException("Error accessing data field rendering data lines.", e);
}
}
// 각 열의 너비 조정
// (renderHeaders에서 이미 trackColumnForAutoSizing 호출됨)
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);
} 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);
}
}
}
@Override

@ -9,4 +9,5 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
String headerName() default "";
int headerWidth() default 0; // 0이면 자동 조정, 0보다 크면 지정된 너비 사용
}

@ -8,11 +8,13 @@ import java.util.Map;
@Getter
public class ExcelMetadata {
private final Map<String, String> excelHeaderNames;
private final Map<String, Integer> excelHeaderWidths;
private final List<String> dataFieldNames;
private final String sheetName;
public ExcelMetadata(Map<String, String> excelHeaderNames, List<String> dataFieldNames, String sheetName) {
public ExcelMetadata(Map<String, String> excelHeaderNames, Map<String, Integer> excelHeaderWidths, List<String> dataFieldNames, String sheetName) {
this.excelHeaderNames = excelHeaderNames;
this.excelHeaderWidths = excelHeaderWidths;
this.dataFieldNames = dataFieldNames;
this.sheetName = sheetName;
}
@ -20,4 +22,8 @@ public class ExcelMetadata {
public String getHeaderName(String fieldName) {
return excelHeaderNames.getOrDefault(fieldName, "");
}
public int getHeaderWidth(String fieldName) {
return excelHeaderWidths.getOrDefault(fieldName, 0);
}
}

@ -20,18 +20,20 @@ public class ExcelMetadataFactory { // (2)
public ExcelMetadata createMetadata(Class<?> clazz) {
Map<String, String> headerNamesMap = new LinkedHashMap<>();
Map<String, Integer> headerWidthsMap = new LinkedHashMap<>();
List<String> dataFieldNamesList = new ArrayList<>();
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());
}
}
if (headerNamesMap.isEmpty()) {
throw new RuntimeException(String.format("Class %s has not @ExcelColumn at all", clazz));
}
return new ExcelMetadata(headerNamesMap, dataFieldNamesList, getSheetName(clazz));
return new ExcelMetadata(headerNamesMap, headerWidthsMap, dataFieldNamesList, getSheetName(clazz));
}
private String getSheetName(Class<?> clazz) {

@ -10,8 +10,13 @@ import java.util.List;
public class ExcelSheetData { // (1)
private final List<?> dataList;
private final Class<?> type;
private final String title; // 제목 행 텍스트 (null이면 제목 행 생성하지 않음)
public static ExcelSheetData of(List<?> dataList, Class<?> type) {
return new ExcelSheetData(dataList, type);
return new ExcelSheetData(dataList, type, null);
}
public static ExcelSheetData of(List<?> dataList, Class<?> type, String title) {
return new ExcelSheetData(dataList, type, title);
}
}

@ -24,8 +24,13 @@ public class SxssfExcelFile extends BaseSxssfExcelFile {
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);
renderDataLines(data, metadata);
writeWithEncryption(stream, password); // if password is null, encryption will not be applied.
}
@ -42,8 +47,13 @@ public class SxssfExcelFile extends BaseSxssfExcelFile {
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);
renderDataLines(data, metadata);
writeWithEncryption(stream, password); // if password is null, encryption will not be applied.
}

@ -20,8 +20,13 @@ public class SxssfMultiSheetExcelFile extends BaseSxssfExcelFile { // (4)
IOException {
for (ExcelSheetData data : dataGroup.getExcelSheetData()) {
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);
renderDataLines(data, metadata);
}
writeWithEncryption(stream, password); // if password is null, encryption will not be applied.
}

@ -448,9 +448,9 @@ public class CrdnRegistAndViewController {
.build())
.collect(Collectors.toList());
// 엑셀 파일 생성 및 다운로드
// 엑셀 파일 생성 및 다운로드 (제목 행 포함)
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, "단속 목록"), request, response, filename);
log.debug("단속 목록 엑셀 다운로드 완료 - 파일명: {}, 건수: {}", filename, excelList.size());
} catch (Exception e) {

Loading…
Cancel
Save