You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

191 lines
6.6 KiB
Java

package egovframework.util.excel;
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.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.security.GeneralSecurityException;
import java.util.List;
import static egovframework.util.excel.SuperClassReflectionUtils.getAllFieldsWithExcelColumn;
public abstract class BaseSxssfExcelFile implements ExcelFile {
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;
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) {
// 시트가 없으면 생성 (제목 행이 없는 경우)
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, ExcelMetadata metadata) {
CellStyle style = 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);
}
}
// 각 열의 너비 조정
// (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
public void write(OutputStream stream) {
try {
workbook.write(stream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@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();
}
workbook.close();
stream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
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.");
}
}
}