excel download 구현

This commit is contained in:
daekuk1
2026-01-21 20:11:59 +09:00
parent d4bdd39450
commit 3c1cd919d7
7 changed files with 727 additions and 0 deletions
@@ -407,6 +407,124 @@
fetchChartData(); fetchChartData();
} }
function exportToExcel() {
console.log('[Excel Export] Starting...');
var postData = {
cmd: 'EXCEL_EXPORT',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val(),
serviceType: '${param.serviceType}'
};
var searchDate = $("input[name=searchDate]").val().replace(/-/g, "");
if (searchDate) {
postData.searchStartDateTime = searchDate + "00";
postData.searchEndDateTime = searchDate + "23";
}
console.log('[Excel Export] Request data:', postData);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 추출
var filename = 'api_stats.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() { function list() {
var gridPostData = getSearchForJqgrid("cmd", "LIST"); var gridPostData = getSearchForJqgrid("cmd", "LIST");
$('#grid').jqGrid({ $('#grid').jqGrid({
@@ -480,6 +598,10 @@
search(); search();
}); });
$("#btn_excel_export").click(function() {
exportToExcel();
});
$("input[name^=search]").keydown(function(key) { $("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) { if (key.keyCode == 13) {
$("#btn_search").click(); $("#btn_search").click();
@@ -510,6 +632,9 @@
<button type="button" class="cssbtn" id="btn_search" level="R"> <button type="button" class="cssbtn" id="btn_search" level="R">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %> <i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button> </button>
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
<i class="material-icons">file_download</i> Excel 다운로드
</button>
</div> </div>
<div class="title" id="title">API 시간별 통계</div> <div class="title" id="title">API 시간별 통계</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
@@ -415,6 +415,127 @@
fetchChartData(range); fetchChartData(range);
} }
function exportToExcel() {
console.log('[Excel Export] Starting...');
var postData = {
cmd: 'EXCEL_EXPORT',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val(),
serviceType: '${param.serviceType}'
};
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, "");
var endDate = $("input[name=searchEndDate]").val().replace(/-/g, "");
var endTime = $("input[name=searchEndTime]").val().replace(/:/g, "");
if (startDate && startTime) {
postData.searchStartDateTime = startDate + startTime;
postData.searchEndDateTime = endDate + endTime;
}
console.log('[Excel Export] Request data:', postData);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 추출
var filename = 'api_stats.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() { function list() {
var gridPostData = getSearchForJqgrid("cmd", "LIST"); var gridPostData = getSearchForJqgrid("cmd", "LIST");
$('#grid').jqGrid({ $('#grid').jqGrid({
@@ -514,6 +635,10 @@
search(); search();
}); });
$("#btn_excel_export").click(function() {
exportToExcel();
});
$("input[name^=search]").keydown(function(key) { $("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) { if (key.keyCode == 13) {
$("#btn_search").click(); $("#btn_search").click();
@@ -549,6 +674,9 @@
<button type="button" class="cssbtn" id="btn_search" level="R"> <button type="button" class="cssbtn" id="btn_search" level="R">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %> <i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button> </button>
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
<i class="material-icons">file_download</i> Excel 다운로드
</button>
</div> </div>
<div class="title" id="title">API 분단위 통계</div> <div class="title" id="title">API 분단위 통계</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
@@ -60,6 +60,47 @@ public class ApiStatsHourService
return repository.findAll(builder, pageable); return repository.findAll(builder, pageable);
} }
public List<ApiStatsHour> selectListForExcel(ApiStatsSearch search) {
QApiStatsHour q = QApiStatsHour.apiStatsHour;
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartDateTime() != null) {
builder.and(q.statTime.goe(search.getParsedStartDateTime()));
builder.and(q.statTime.loe(search.getParsedEndDateTime()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
if (StringUtils.isNotBlank(search.getSearchInboundAdapter())) {
builder.and(q.inboundAdapter.containsIgnoreCase(search.getSearchInboundAdapter()));
}
if (StringUtils.isNotBlank(search.getSearchOutboundAdapter())) {
builder.and(q.outboundAdapter.containsIgnoreCase(search.getSearchOutboundAdapter()));
}
return getJPAQueryFactory()
.selectFrom(q)
.where(builder)
.orderBy(q.statTime.desc())
.fetch();
}
public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) { public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) {
QApiStatsHour q = QApiStatsHour.apiStatsHour; QApiStatsHour q = QApiStatsHour.apiStatsHour;
BooleanBuilder builder = new BooleanBuilder(); BooleanBuilder builder = new BooleanBuilder();
@@ -62,6 +62,47 @@ public class ApiStatsMinuteService
return repository.findAll(builder, pageable); return repository.findAll(builder, pageable);
} }
public List<ApiStatsMinute> selectListForExcel(ApiStatsSearch search) {
QApiStatsMinute q = QApiStatsMinute.apiStatsMinute;
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartDateTime() != null) {
builder.and(q.statTime.goe(search.getParsedStartDateTime()));
builder.and(q.statTime.loe(search.getParsedEndDateTime()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
if (StringUtils.isNotBlank(search.getSearchInboundAdapter())) {
builder.and(q.inboundAdapter.containsIgnoreCase(search.getSearchInboundAdapter()));
}
if (StringUtils.isNotBlank(search.getSearchOutboundAdapter())) {
builder.and(q.outboundAdapter.containsIgnoreCase(search.getSearchOutboundAdapter()));
}
return getJPAQueryFactory()
.selectFrom(q)
.where(builder)
.orderBy(q.statTime.desc())
.fetch();
}
public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) { public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) {
QApiStatsMinute q = QApiStatsMinute.apiStatsMinute; QApiStatsMinute q = QApiStatsMinute.apiStatsMinute;
BooleanBuilder builder = new BooleanBuilder(); BooleanBuilder builder = new BooleanBuilder();
@@ -1,7 +1,14 @@
package com.eactive.ext.kjb.statistics; package com.eactive.ext.kjb.statistics;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@@ -15,6 +22,7 @@ import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService;
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper; import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@@ -29,6 +37,7 @@ public class ApiStatsHourController {
private final ApiStatsHourService service; private final ApiStatsHourService service;
private final ApiStatsUIMapper mapper; private final ApiStatsUIMapper mapper;
private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiStatsHourMan.view") @GetMapping(value = "/onl/kjb/statistics/apiStatsHourMan.view")
public String view() { public String view() {
@@ -53,4 +62,54 @@ public class ApiStatsHourController {
List<ApiStatsChartUI> chartData = service.selectChartData(search); List<ApiStatsChartUI> chartData = service.selectChartData(search);
return ResponseEntity.ok(chartData); return ResponseEntity.ok(chartData);
} }
@PostMapping(value = "/onl/kjb/statistics/apiStatsHourMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search);
try {
List<ApiStatsHour> dataList = service.selectListForExcel(search);
log.info("Data retrieved - count: {}", dataList.size());
if (dataList.isEmpty()) {
log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
response.getWriter().flush();
return;
}
List<ApiStatsUI> uiList = dataList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
log.info("Data converted to UI list - count: {}", uiList.size());
String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName);
excelExportService.exportApiStats(uiList, fileName, response);
log.info("Excel export completed successfully");
} catch (Exception e) {
log.error("Excel export failed - error: {}", e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
response.getWriter().flush();
} else {
log.error("Cannot send error response - response already committed");
}
}
}
private String generateFileName(ApiStatsSearch search) {
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime())
? search.getSearchStartDateTime()
: LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHH"));
return "API시간별통계_" + timeRange + ".xlsx";
}
} }
@@ -1,7 +1,14 @@
package com.eactive.ext.kjb.statistics; package com.eactive.ext.kjb.statistics;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@@ -15,6 +22,7 @@ import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinuteService; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinuteService;
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper; import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@@ -29,6 +37,7 @@ public class ApiStatsMinuteController {
private final ApiStatsMinuteService service; private final ApiStatsMinuteService service;
private final ApiStatsUIMapper mapper; private final ApiStatsUIMapper mapper;
private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.view") @GetMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.view")
public String view() { public String view() {
@@ -53,4 +62,54 @@ public class ApiStatsMinuteController {
List<ApiStatsChartUI> chartData = service.selectChartData(search); List<ApiStatsChartUI> chartData = service.selectChartData(search);
return ResponseEntity.ok(chartData); return ResponseEntity.ok(chartData);
} }
@PostMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search);
try {
List<ApiStatsMinute> dataList = service.selectListForExcel(search);
log.info("Data retrieved - count: {}", dataList.size());
if (dataList.isEmpty()) {
log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
response.getWriter().flush();
return;
}
List<ApiStatsUI> uiList = dataList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
log.info("Data converted to UI list - count: {}", uiList.size());
String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName);
excelExportService.exportApiStats(uiList, fileName, response);
log.info("Excel export completed successfully");
} catch (Exception e) {
log.error("Excel export failed - error: {}", e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
response.getWriter().flush();
} else {
log.error("Cannot send error response - response already committed");
}
}
}
private String generateFileName(ApiStatsSearch search) {
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime())
? search.getSearchStartDateTime()
: LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
return "API분단위통계_" + timeRange + ".xlsx";
}
} }
@@ -0,0 +1,274 @@
package com.eactive.ext.kjb.statistics.service;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
import lombok.extern.slf4j.Slf4j;
/**
* API 통계 Excel Export 서비스
* Hour/Minute 통계 데이터를 Excel 파일로 생성
*/
@Slf4j
@Service
public class ApiStatsExcelExportService {
private static final String[] COLUMN_HEADERS = {
"통계시간", "API명", "인스턴스", "업무구분", "클라이언트ID",
"Inbound Adapter", "Outbound Adapter",
"총건수", "성공건수", "Timeout건수", "시스템오류건수", "업무오류건수",
"Seq900 Timeout", "Seq900 시스템오류", "Seq900 업무오류",
"평균응답시간", "최소응답시간", "최대응답시간", "P50응답시간", "P95응답시간"
};
/**
* API 통계 데이터를 Excel 파일로 생성하여 HTTP 응답으로 전송
*
* @param dataList API 통계 데이터 리스트
* @param fileName 다운로드 파일명
* @param response HTTP 응답 객체
* @throws IOException 파일 생성 실패 시
*/
public void exportApiStats(List<ApiStatsUI> dataList, String fileName, HttpServletResponse response)
throws IOException {
log.info("Starting Excel export - rows: {}, filename: {}", dataList.size(), fileName);
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("API통계");
log.debug("Excel sheet created");
// 스타일 생성
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle stringStyle = createStringStyle(workbook);
CellStyle numberStyle = createNumberStyle(workbook);
CellStyle decimalStyle = createDecimalStyle(workbook);
log.debug("Excel styles created");
// 헤더 행 생성
createHeaderRow(sheet, headerStyle);
log.debug("Header row created");
// 데이터 행 생성
int rowNum = 1;
for (ApiStatsUI data : dataList) {
createDataRow(sheet, rowNum++, data, stringStyle, numberStyle, decimalStyle);
}
log.info("Data rows created - count: {}", dataList.size());
// 컬럼 너비 자동 조정
autoSizeColumns(sheet);
log.debug("Column widths adjusted");
// HTTP 응답 설정
setHttpResponse(response, fileName, workbook);
log.info("Excel file sent to response");
} catch (Exception e) {
log.error("Error creating Excel file", e);
throw new IOException("Excel file creation failed: " + e.getMessage(), e);
}
}
/**
* 헤더 스타일 생성 (회색 배경, 볼드, 테두리)
*/
private CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
// 배경색
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 테두리
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
// 정렬
style.setAlignment(HorizontalAlignment.CENTER);
// 폰트 (볼드)
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}
/**
* 문자열 데이터 스타일 생성 (기본 테두리)
*/
private CellStyle createStringStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
return style;
}
/**
* 숫자 데이터 스타일 생성 (우측정렬 + 쉼표 구분)
*/
private CellStyle createNumberStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("#,##0"));
return style;
}
/**
* 소수점 데이터 스타일 생성 (우측정렬 + 소수점 3자리)
*/
private CellStyle createDecimalStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("0.000"));
return style;
}
/**
* 헤더 행 생성
*/
private void createHeaderRow(Sheet sheet, CellStyle headerStyle) {
Row headerRow = sheet.createRow(0);
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(COLUMN_HEADERS[i]);
cell.setCellStyle(headerStyle);
}
}
/**
* 데이터 행 생성
*/
private void createDataRow(Sheet sheet, int rowNum, ApiStatsUI data,
CellStyle stringStyle, CellStyle numberStyle, CellStyle decimalStyle) {
Row row = sheet.createRow(rowNum);
int colNum = 0;
// 문자열 컬럼 (0-6)
createStringCell(row, colNum++, data.getStatTime(), stringStyle);
createStringCell(row, colNum++, data.getApiName(), stringStyle);
createStringCell(row, colNum++, data.getGwInstanceId(), stringStyle);
createStringCell(row, colNum++, data.getBizDivCode(), stringStyle);
createStringCell(row, colNum++, data.getClientId(), stringStyle);
createStringCell(row, colNum++, data.getInboundAdapter(), stringStyle);
createStringCell(row, colNum++, data.getOutboundAdapter(), stringStyle);
// 숫자 컬럼 (7-14)
createLongCell(row, colNum++, data.getTotalCnt(), numberStyle);
createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle);
createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle);
createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle);
createLongCell(row, colNum++, data.getBizErrCnt(), numberStyle);
createLongCell(row, colNum++, data.getSeq900TimeoutCnt(), numberStyle);
createLongCell(row, colNum++, data.getSeq900SystemErrCnt(), numberStyle);
createLongCell(row, colNum++, data.getSeq900BizErrCnt(), numberStyle);
// 소수점 컬럼 (15-19)
createDecimalCell(row, colNum++, data.getAvgRespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getMinRespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getMaxRespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getP50RespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getP95RespTime(), decimalStyle);
}
/**
* 문자열 셀 생성
*/
private void createStringCell(Row row, int colNum, String value, CellStyle style) {
Cell cell = row.createCell(colNum);
cell.setCellValue(value != null ? value : "");
cell.setCellStyle(style);
}
/**
* Long 타입 숫자 셀 생성
*/
private void createLongCell(Row row, int colNum, Long value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0);
}
cell.setCellStyle(style);
}
/**
* BigDecimal 타입 소수점 셀 생성
*/
private void createDecimalCell(Row row, int colNum, BigDecimal value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0.0);
}
cell.setCellStyle(style);
}
/**
* 컬럼 너비 자동 조정
*/
private void autoSizeColumns(Sheet sheet) {
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
sheet.autoSizeColumn(i);
// 한글 문자 고려하여 약간 여유 공간 추가
sheet.setColumnWidth(i, sheet.getColumnWidth(i) + 512);
}
}
/**
* HTTP 응답 설정 및 파일 전송
*/
private void setHttpResponse(HttpServletResponse response, String fileName, Workbook workbook)
throws IOException {
// Content-Type 설정
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// Content-Disposition 설정 (파일명 UTF-8 인코딩)
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString())
.replaceAll("\\+", "%20");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
// Workbook을 응답 스트림으로 전송
workbook.write(response.getOutputStream());
response.getOutputStream().flush();
}
}