diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsHourMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsHourMan.jsp
index e9ea8c7..0fabaf4 100644
--- a/WebContent/jsp/onl/kjb/statistics/apiStatsHourMan.jsp
+++ b/WebContent/jsp/onl/kjb/statistics/apiStatsHourMan.jsp
@@ -407,6 +407,124 @@
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() {
var gridPostData = getSearchForJqgrid("cmd", "LIST");
$('#grid').jqGrid({
@@ -480,6 +598,10 @@
search();
});
+ $("#btn_excel_export").click(function() {
+ exportToExcel();
+ });
+
$("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) {
$("#btn_search").click();
@@ -510,6 +632,9 @@
+
API 시간별 통계
diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsMinuteMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsMinuteMan.jsp
index 203e86f..b94b52b 100644
--- a/WebContent/jsp/onl/kjb/statistics/apiStatsMinuteMan.jsp
+++ b/WebContent/jsp/onl/kjb/statistics/apiStatsMinuteMan.jsp
@@ -415,6 +415,127 @@
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() {
var gridPostData = getSearchForJqgrid("cmd", "LIST");
$('#grid').jqGrid({
@@ -514,6 +635,10 @@
search();
});
+ $("#btn_excel_export").click(function() {
+ exportToExcel();
+ });
+
$("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) {
$("#btn_search").click();
@@ -549,6 +674,9 @@
+
API 분단위 통계
diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java
index ceadd5a..74816c8 100644
--- a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java
+++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java
@@ -60,6 +60,47 @@ public class ApiStatsHourService
return repository.findAll(builder, pageable);
}
+ public List 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 selectChartData(ApiStatsSearch search) {
QApiStatsHour q = QApiStatsHour.apiStatsHour;
BooleanBuilder builder = new BooleanBuilder();
diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteService.java
index b16b6e7..9a35b35 100644
--- a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteService.java
+++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMinuteService.java
@@ -62,6 +62,47 @@ public class ApiStatsMinuteService
return repository.findAll(builder, pageable);
}
+ public List 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 selectChartData(ApiStatsSearch search) {
QApiStatsMinute q = QApiStatsMinute.apiStatsMinute;
BooleanBuilder builder = new BooleanBuilder();
diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsHourController.java b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsHourController.java
index 1b11f19..1869eba 100644
--- a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsHourController.java
+++ b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsHourController.java
@@ -1,7 +1,14 @@
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.stream.Collectors;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
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.ApiStatsHourService;
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.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@@ -29,6 +37,7 @@ public class ApiStatsHourController {
private final ApiStatsHourService service;
private final ApiStatsUIMapper mapper;
+ private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiStatsHourMan.view")
public String view() {
@@ -53,4 +62,54 @@ public class ApiStatsHourController {
List chartData = service.selectChartData(search);
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 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 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";
+ }
}
diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMinuteController.java b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMinuteController.java
index a1b9021..baf46a5 100644
--- a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMinuteController.java
+++ b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMinuteController.java
@@ -1,7 +1,14 @@
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.stream.Collectors;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
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.ApiStatsMinuteService;
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.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@@ -29,6 +37,7 @@ public class ApiStatsMinuteController {
private final ApiStatsMinuteService service;
private final ApiStatsUIMapper mapper;
+ private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.view")
public String view() {
@@ -53,4 +62,54 @@ public class ApiStatsMinuteController {
List chartData = service.selectChartData(search);
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 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 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";
+ }
}
diff --git a/src/main/java/com/eactive/ext/kjb/statistics/service/ApiStatsExcelExportService.java b/src/main/java/com/eactive/ext/kjb/statistics/service/ApiStatsExcelExportService.java
new file mode 100644
index 0000000..a97c2fe
--- /dev/null
+++ b/src/main/java/com/eactive/ext/kjb/statistics/service/ApiStatsExcelExportService.java
@@ -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 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();
+ }
+}