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(); + } +}