diff --git a/WebContent/jsp/common/scheduler/schedulerManDetail.jsp b/WebContent/jsp/common/scheduler/schedulerManDetail.jsp index 3851e26..82cca5b 100644 --- a/WebContent/jsp/common/scheduler/schedulerManDetail.jsp +++ b/WebContent/jsp/common/scheduler/schedulerManDetail.jsp @@ -465,21 +465,21 @@ $(document).ready(function() { - - - - - - + + + + + + - - - - - - + + + + + +
Default SchedulerOn Memory: Previous Fired: Next Fire Time: isConcurrentExectionDisallowed: PersistJobData: Durable: On Memory: Previous Fired: Next Fire Time: isConcurrentExectionDisallowed: PersistJobData: Durable:
Clustered SchedulerOn Memory: Previous Fired: Next Fire Time: isConcurrentExectionDisallowed: PersistJobData: Durable: On Memory: Previous Fired: Next Fire Time: isConcurrentExectionDisallowed: PersistJobData: Durable:
diff --git a/WebContent/jsp/onl/apim/portalnotice/portalNoticeManDetail.jsp b/WebContent/jsp/onl/apim/portalnotice/portalNoticeManDetail.jsp index c26dad6..6e74a8d 100644 --- a/WebContent/jsp/onl/apim/portalnotice/portalNoticeManDetail.jsp +++ b/WebContent/jsp/onl/apim/portalnotice/portalNoticeManDetail.jsp @@ -297,7 +297,7 @@ - + diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp index 8322d71..5bc3e2f 100644 --- a/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp +++ b/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp @@ -82,6 +82,54 @@ + + + +
+
+ +
+
+
+ + +
+
API 사용현황
+ + + + + + + + + + + + + + + +
조회기간 + + ~ + + (최대 31일) +
조회구분 + + + + 제휴사명 + + API명 + +
+ +
+
+
+
+ + +
+
+ + diff --git a/WebContent/jsp/onl/transaction/apim/apiInterfaceManDetail.jsp b/WebContent/jsp/onl/transaction/apim/apiInterfaceManDetail.jsp index b0c6b3a..7944b82 100644 --- a/WebContent/jsp/onl/transaction/apim/apiInterfaceManDetail.jsp +++ b/WebContent/jsp/onl/transaction/apim/apiInterfaceManDetail.jsp @@ -1624,12 +1624,12 @@ x-effect="if (apiInterface.syncAsyncType !== 'async') apiInterface.requestType = 'S'"> + value="S" :disabled="$store.formState.isReqResDisabled"> + value="R" :disabled="$store.formState.isReqResDisabled"> diff --git a/src/main/java/com/eactive/eai/rms/common/scheduler/SchedulerManService.java b/src/main/java/com/eactive/eai/rms/common/scheduler/SchedulerManService.java index 0b10e8f..35d6e24 100644 --- a/src/main/java/com/eactive/eai/rms/common/scheduler/SchedulerManService.java +++ b/src/main/java/com/eactive/eai/rms/common/scheduler/SchedulerManService.java @@ -1,13 +1,11 @@ package com.eactive.eai.rms.common.scheduler; -import com.eactive.eai.rms.common.base.BaseService; -import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper; -import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI; -import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo; -import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch; -import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService; -import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService; -import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + import org.apache.commons.lang3.StringUtils; import org.quartz.JobDetail; import org.quartz.JobKey; @@ -20,9 +18,14 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; +import com.eactive.eai.rms.common.base.BaseService; +import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper; +import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI; +import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo; +import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch; +import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService; +import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService; +import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService; @Service("schedulerService") @Transactional(transactionManager = "transactionManagerForEMS") @@ -122,10 +125,10 @@ public class SchedulerManService extends BaseService { List triggersOfJob = scheduler.getTriggersOfJob(new JobKey(jobName, Scheduler.DEFAULT_GROUP)); if(triggersOfJob.size() > 0) { - result.setStartTime(String.valueOf(triggersOfJob.get(0).getStartTime())); - result.setEndTime(String.valueOf(triggersOfJob.get(0).getEndTime())); - result.setPreviousFireTime(String.valueOf(triggersOfJob.get(0).getPreviousFireTime())); - result.setNextFireTime(String.valueOf(triggersOfJob.get(0).getNextFireTime())); + result.setStartTime(dateFormat(triggersOfJob.get(0).getStartTime())); + result.setEndTime(dateFormat(triggersOfJob.get(0).getEndTime())); + result.setPreviousFireTime(dateFormat(triggersOfJob.get(0).getPreviousFireTime())); + result.setNextFireTime(dateFormat(triggersOfJob.get(0).getNextFireTime())); } } catch (Exception e) { @@ -150,5 +153,14 @@ public class SchedulerManService extends BaseService { public void delete(String jobName) { jobInfoService.deleteById(jobName); } + + private String dateFormat(Date date) { + if (date == null) { + return ""; + } else { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.format(date); + } + } } diff --git a/src/main/java/com/eactive/eai/rms/common/util/StringUtils.java b/src/main/java/com/eactive/eai/rms/common/util/StringUtils.java index 2376dd1..a6f02c1 100644 --- a/src/main/java/com/eactive/eai/rms/common/util/StringUtils.java +++ b/src/main/java/com/eactive/eai/rms/common/util/StringUtils.java @@ -1,5 +1,6 @@ package com.eactive.eai.rms.common.util; +import java.math.BigDecimal; import java.util.List; import org.apache.log4j.Logger; @@ -191,6 +192,20 @@ public class StringUtils } return sb.toString(); } + + public static String toString(Object val) { + return val != null ? val.toString() : null; + } + + public static Long toLong(Object val) { + return val != null ? ((Number) val).longValue() : null; + } + + public static BigDecimal toDecimal(Object val) { + if (val == null) return null; + if (val instanceof BigDecimal) return (BigDecimal) val; + return new BigDecimal(val.toString()); + } // // /** // * - "" 나 Null 을 입력받아 SPACE 로 변환한다. diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayService.java index ad7fc2e..efe1566 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayService.java @@ -120,11 +120,11 @@ public class ApiStatsDayService q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), - // 가중 평균: SUM(avg * successCnt) / SUM(successCnt) + // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt) Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) 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 91524a7..6d01415 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 @@ -211,11 +211,10 @@ public class ApiStatsHourService q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), - // 가중 평균: SUM(avg * successCnt) / SUM(successCnt) Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) @@ -293,9 +292,9 @@ public class ApiStatsHourService q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) 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 7466422..964fef6 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 @@ -214,11 +214,11 @@ public class ApiStatsMinuteService q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), - // 가중 평균: SUM(avg * successCnt) / SUM(successCnt) + // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt) Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthService.java index 4545eca..6bb2e3a 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthService.java @@ -120,11 +120,11 @@ public class ApiStatsMonthService q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), - // 가중 평균: SUM(avg * successCnt) / SUM(successCnt) + // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt) Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) @@ -172,9 +172,9 @@ public class ApiStatsMonthService q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearService.java index 65842b0..8d2f080 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearService.java @@ -117,11 +117,11 @@ public class ApiStatsYearService q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), - // 가중 평균: SUM(avg * successCnt) / SUM(successCnt) + // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt) Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) @@ -169,9 +169,9 @@ public class ApiStatsYearService q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) diff --git a/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsExcelExportService.java b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsExcelExportService.java new file mode 100644 index 0000000..b79aae2 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsExcelExportService.java @@ -0,0 +1,256 @@ +package com.eactive.eai.rms.data.ext.djb.statistics; + +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 ApiUseStatsExcelExportService { + + private static final String[] COLUMN_HEADERS = { + "구분", "총건수", "성공", "성공율(%)", "실패율(%)", "Timeout", "시스템오류", + "평균응답(ms)", "최소응답(ms)", "최대응답(ms)" + }; + + /** + * 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; + } + + /** + * 소수점 데이터 스타일 생성 (우측정렬 + 소수점 2자리) + */ + 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.00")); + 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; + + createStringCell(row, colNum++, data.getOrgName(), stringStyle); + createLongCell(row, colNum++, data.getTotalCnt(), numberStyle); + createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle); + createDecimalCell(row, colNum++, data.getSuccessRate(), decimalStyle); + createDecimalCell(row, colNum++, data.getFailRate(), decimalStyle); + createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle); + createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle); + createDecimalCell(row, colNum++, data.getAvgRespTime(), numberStyle); + createDecimalCell(row, colNum++, data.getMinRespTime(), numberStyle); + createDecimalCell(row, colNum++, data.getMaxRespTime(), numberStyle); + } + + /** + * 문자열 셀 생성 + */ + 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(); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsRepository.java b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsRepository.java new file mode 100644 index 0000000..055127d --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsRepository.java @@ -0,0 +1,12 @@ +package com.eactive.eai.rms.data.ext.djb.statistics; + +import com.eactive.eai.data.jpa.BaseRepository; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId; + +/** + * API 사용현황 Repository + */ +interface ApiUseStatsRepository extends BaseRepository { + +} diff --git a/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsService.java b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsService.java new file mode 100644 index 0000000..e6923f6 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsService.java @@ -0,0 +1,211 @@ +package com.eactive.eai.rms.data.ext.djb.statistics; + +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.data.jpa.AbstractDataService; +import com.eactive.eai.rms.common.util.StringUtils; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId; +import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; +import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; + +@Service +@Transactional +public class ApiUseStatsService + extends AbstractDataService { + + private static final int MAX_DAYS = 31; + + @PersistenceContext + private EntityManager entityManager; + + @SuppressWarnings("unchecked") + public Page selectList(ApiStatsSearch search, Pageable pageable) { + Query dataQuery = getDataQuery(search); + dataQuery.setFirstResult((int) pageable.getOffset()); + dataQuery.setMaxResults(pageable.getPageSize()); + + List rows = dataQuery.getResultList(); + List results = rows.stream() + .map(this::toVO) + .collect(Collectors.toList()); + + long records = rows.size() > 0 ? StringUtils.toLong(rows.get(0)[0]) : 0; + + return new PageImpl<>(results, pageable, records); + } + + + @SuppressWarnings("unchecked") + public List selectList(ApiStatsSearch search) { + Query dataQuery = getDataQuery(search); + List rows = dataQuery.getResultList(); + return rows.stream() + .map(this::toVO) + .collect(Collectors.toList()); + } + + + private Query getDataQuery(ApiStatsSearch search) { + + String schemaEms = System.getProperty("eai.tableowner", "EMSADM").toUpperCase(); + + StringBuilder where = buildNativeWhere(search); + + String dataSql = ""; + if ("ORG".equals(search.getSearchType())) { + dataSql = "SELECT COUNT(*) OVER () AS RECORDS " + + ", B.ORGNAME" + + ", SUM(A.TOTAL_CNT)" + + ", SUM(A.SUCCESS_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", SUM(A.TIMEOUT_CNT)" + + ", SUM(A.SYSTEM_ERR_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END" + + ", MIN(A.MIN_RESP_TIME)" + + ", MAX(A.MAX_RESP_TIME)" + + " FROM API_STATS_DAY A" + + " LEFT OUTER JOIN " + schemaEms + ".PTL_CREDENTIAL B ON A.CLIENT_ID = B.CLIENTID" + + " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME " + + where + + " GROUP BY B.ORGNAME" + + " ORDER BY ORGNAME"; + } else if ("API".equals(search.getSearchType())) { + dataSql = "SELECT COUNT(*) OVER () AS RECORDS " + + ", C.EAISVCDESC" + + ", SUM(A.TOTAL_CNT)" + + ", SUM(A.SUCCESS_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", SUM(A.TIMEOUT_CNT)" + + ", SUM(A.SYSTEM_ERR_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END" + + ", MIN(A.MIN_RESP_TIME)" + + ", MAX(A.MAX_RESP_TIME)" + + " FROM API_STATS_DAY A " + + " LEFT OUTER JOIN " + schemaEms + ".PTL_CREDENTIAL B ON A.CLIENT_ID = B.CLIENTID" + + " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME " + + where + + " GROUP BY C.EAISVCDESC" + + " ORDER BY EAISVCDESC"; + } else if ("DATE".equals(search.getSearchType())) { + dataSql = "SELECT COUNT(*) OVER () AS RECORDS " + + ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME " + + ", SUM(A.TOTAL_CNT)" + + ", SUM(A.SUCCESS_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", SUM(A.TIMEOUT_CNT)" + + ", SUM(A.SYSTEM_ERR_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END" + + ", MIN(A.MIN_RESP_TIME)" + + ", MAX(A.MAX_RESP_TIME)" + + " FROM API_STATS_DAY A " + + " LEFT OUTER JOIN " + schemaEms + ".PTL_CREDENTIAL B ON A.CLIENT_ID = B.CLIENTID" + + " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME " + + where + + " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')" + + " ORDER BY STAT_TIME"; + } + + Query dataQuery = entityManager.createNativeQuery(dataSql); + if (search.getParsedStartDate() != null) { + dataQuery.setParameter("searchStartDate", search.getParsedStartDate()); + dataQuery.setParameter("searchEndDate", search.getParsedEndDate()); + } + if (StringUtils.isNotBlank(search.getSearchOrgName())) { + dataQuery.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%"); + } + if (StringUtils.isNotBlank(search.getSearchApiName())) { + dataQuery.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%"); + } + return dataQuery; + } + + + /** + * Native Query 결과를 ApiStatsUI로 변환 + */ + private ApiStatsUI toVO(Object[] row) { + ApiStatsUI vo = new ApiStatsUI(); + vo.setOrgName(StringUtils.toString(row[1])); + vo.setTotalCnt(StringUtils.toLong(row[2])); + vo.setSuccessCnt(StringUtils.toLong(row[3])); + vo.setSuccessRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP)); + vo.setFailRate(StringUtils.toDecimal(row[5]).setScale(2, RoundingMode.HALF_UP)); + vo.setTimeoutCnt(StringUtils.toLong(row[6])); + vo.setSystemErrCnt(StringUtils.toLong(row[7])); + vo.setAvgRespTime(StringUtils.toDecimal(row[8])); + vo.setMinRespTime(StringUtils.toDecimal(row[9])); + vo.setMaxRespTime(StringUtils.toDecimal(row[10])); + return vo; + } + + + /** + * native SQL용 동적 WHERE 절 생성 + */ + private StringBuilder buildNativeWhere(ApiStatsSearch search) { + StringBuilder where = new StringBuilder(" WHERE 1=1"); + + calculateDateRange(search); + + if (search.getParsedStartDate() != null) { + where.append(" AND A.STAT_TIME >= :searchStartDate "); + where.append(" AND A.STAT_TIME <= :searchEndDate "); + } + + if (StringUtils.isNotBlank(search.getSearchOrgName())) { + where.append(" AND UPPER(B.ORGNAME) LIKE :searchOrgName "); + } + + if (StringUtils.isNotBlank(search.getSearchApiName())) { + where.append(" AND UPPER(C.EAISVCDESC) LIKE :searchApiName "); + } + + return where; + } + + + /** + * 조회 기간 계산 (최대 31일 제한) + */ + private void calculateDateRange(ApiStatsSearch search) { + if (StringUtils.isBlank(search.getSearchStartDateTime())) { + return; + } + + LocalDate startDate = LocalDate.parse(search.getSearchStartDateTime(), + DateTimeFormatter.ofPattern("yyyyMMdd")); + LocalDate endDate; + + if (StringUtils.isNotBlank(search.getSearchEndDateTime())) { + endDate = LocalDate.parse(search.getSearchEndDateTime(), + DateTimeFormatter.ofPattern("yyyyMMdd")); + if (java.time.Period.between(startDate, endDate).getDays() > MAX_DAYS) { + endDate = startDate.plusDays(MAX_DAYS); + } + } else { + endDate = startDate.plusDays(MAX_DAYS); + } + + search.setParsedStartDate(startDate); + search.setParsedEndDate(endDate); + } + +} diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java b/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java new file mode 100644 index 0000000..606f50c --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java @@ -0,0 +1,144 @@ +package com.eactive.eai.rms.ext.djb.job; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; +import com.eactive.eai.rms.common.datasource.DataSourceType; +import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; +import com.eactive.eai.rms.common.util.CommonUtil; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsHour; + +/** + * API 로그 테이블을 시간별 통계로 집계하는 배치 작업 + * TSEAILGXX → API_STATS_HOUR + * 실행 주기: 매시 00:10 (당일 데이터 집계) + */ +@Component +public class ApiStatsHourlyAggregationJob implements Job { + private static final Logger log = LoggerFactory.getLogger(ApiStatsHourlyAggregationJob.class); + + @PersistenceContext + private EntityManager entityManager; + + private ApiStatsHourService apiStatsHourService; + + @Autowired + public void setApiStatsHourService(ApiStatsHourService apiStatsHourService) { + this.apiStatsHourService = apiStatsHourService; + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + ApplicationContext appContext = null; + final ApiStatsHourlyAggregationJob selfJob; + try { + appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext"); + selfJob = appContext.getBean(ApiStatsHourlyAggregationJob.class); + } catch (SchedulerException e) { + log.error("applicationContext get module error", e); + return; + } + + DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW); + DataSourceContextHolder.setDataSourceType(dataType); + try { + LocalDate targetDate = LocalDate.now(); + if (LocalTime.now().isBefore(LocalTime.of(1, 0))) { + targetDate = targetDate.minusDays(1); + } + selfJob.executeManual(targetDate); + } catch (Exception e) { + throw new JobExecutionException(e); + } finally { + DataSourceContextHolder.clearDataSourceType(); + } + } + + /** + * 수동 실행 메서드 (날짜 지정) + */ + @Transactional + public int executeManual(LocalDate targetDate) { + long startTime = System.currentTimeMillis(); + log.info("=== 시간별 통계 집계 작업 시작 === 대상: {}", targetDate); + + String searchDate = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String schemaEMS = System.getProperty("eai.tableowner", "EMSADM").toUpperCase(); + String logTableName = CommonUtil.getLogTable(searchDate, true); + + // 1. 기존 데이터 삭제 + QApiStatsHour q = QApiStatsHour.apiStatsHour; + long deletedCount = apiStatsHourService.getJPAQueryFactory() + .delete(q) + .where(q.statTime.goe(targetDate.atStartOfDay()) + .and(q.statTime.lt(targetDate.plusDays(1).atStartOfDay()))) + .execute(); + log.info("기존 데이터 삭제: {} 건", deletedCount); + + // 2. 집계 및 데이터 입력 + /** + * EAISVCSERNO로 그룹핑하여 1행으로 집계를 한 다음 (한거래 안에서 adapter가 여러개 있을 경우 seq=100의 adapter를 기준) + * 총건수 : EAISVCSERNO로 그룹핑한 총 건수 + * 성공건수 : seq = 400 and EAIERRCD = null 인 건수 + * Timeout : EAIERRCD in (공통코드 CODEGROUP = 'ERRCODE_TIMEOUT') 인 건수 + * 시스템오류 : (seq400 = null or EAIERRCD is not null) and 공통코드(ERRCODE_TIMEOUT)에 정의되지 않은 errorcode 인 건수 + */ + String sql = + "INSERT INTO API_STATS_HOUR" + + " (STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID" + + " , INBOUND_ADAPTER, OUTBOUND_ADAPTER" + + ", TOTAL_CNT, SUCCESS_CNT, TIMEOUT_CNT, SYSTEM_ERR_CNT, BIZ_ERR_CNT" + + ", SEQ900_TIMEOUT_CNT, SEQ900_SYSTEM_ERR_CNT, SEQ900_BIZ_ERR_CNT" + + ", AVG_RESP_TIME, MIN_RESP_TIME, MAX_RESP_TIME, P50_RESP_TIME, P95_RESP_TIME)" + + " SELECT TO_TIMESTAMP(SUBSTR(DT,1,10) || '0000', 'YYYYMMDDHH24MISS')" + + " , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" + + " , INBOUND_ADAPTER, OUTBOUND_ADAPTER" + + " , COUNT(EAISVCSERNO)" + + " , SUM(CASE WHEN A.ERROR_CODE IS NULL AND E400 IS NOT NULL THEN 1 ELSE 0 END)" + + " , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" + + " , SUM(CASE WHEN (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.CODE IS NULL THEN 1 ELSE 0 END)" + + " , 0, 0, 0, 0" + + " , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" + + " FROM (" + + " SELECT EAISVCSERNO" + + " , MAX(EAISVCNAME) AS API_NAME, MAX(EAISEVRINSTNCNAME) AS GW_INSTANCE_ID" + + " , MAX(EAIBZWKDSTCD) AS BIZ_DIV_CODE, MAX(CLIENTID) AS CLIENT_ID" + + " , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN GSTATSYSADPTRBZWKGROUPNAME ELSE '' END) AS INBOUND_ADAPTER" + + " , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN PSVSYSADPTRBZWKGROUPNAME ELSE '' END) AS OUTBOUND_ADAPTER" + + " , MIN(MSGDPSTYMS) AS DT" + + " , MAX(MSGPRCSSYMS) - MIN(MSGDPSTYMS) AS RESP_TIME" + + " , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" + + " , MAX(EAIERRCD) AS ERROR_CODE" + + " FROM " + logTableName + + " WHERE MSGDPSTYMS LIKE :searchDate || '%'" + + " GROUP BY EAISVCSERNO" + + " ) A LEFT OUTER JOIN " + schemaEMS + ".TSEAIRM28 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT'" + + " GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" + + " , INBOUND_ADAPTER, OUTBOUND_ADAPTER"; + + int savedCount = entityManager.createNativeQuery(sql) + .setParameter("searchDate", searchDate) + .executeUpdate(); + + long elapsedTime = System.currentTimeMillis() - startTime; + log.info("=== 집계 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime); + return savedCount; + } +} diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/statistics/ApiUseStatsController.java b/src/main/java/com/eactive/eai/rms/ext/djb/statistics/ApiUseStatsController.java index 3b79e3c..8676b9f 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/statistics/ApiUseStatsController.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/statistics/ApiUseStatsController.java @@ -1,29 +1,22 @@ package com.eactive.eai.rms.ext.djb.statistics; import java.io.IOException; -import java.time.LocalDate; +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; -import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import com.eactive.eai.rms.common.vo.GridResponse; -import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay; -import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService; -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.eai.rms.data.ext.djb.statistics.ApiUseStatsExcelExportService; +import com.eactive.eai.rms.data.ext.djb.statistics.ApiUseStatsService; import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; @@ -35,37 +28,29 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class ApiUseStatsController { - private final ApiStatsDayService service; - private final ApiStatsUIMapper mapper; - private final ApiStatsExcelExportService excelExportService; + private final ApiUseStatsService service; + private final ApiUseStatsExcelExportService excelExportService; - @GetMapping(value = "/onl/djb/statistics/apiUseStatsMan.view") + @GetMapping(value = "/onl/kjb/statistics/apiUseStatsMan.view") public String view() { - return "/onl/djb/statistics/apiUseStatsMan"; + return "/onl/kjb/statistics/apiUseStatsMan"; } - @PostMapping(value = "/onl/djb/statistics/apiUseStatsMan.json", params = "cmd=LIST") + @PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=LIST") public ResponseEntity> selectList(ApiStatsSearch search, Pageable pageable) { - Pageable sortedPageable = PageRequest.of( - pageable.getPageNumber(), - pageable.getPageSize(), - Sort.by(Sort.Order.desc("statTime"))); - - Page page = service.selectList(search, sortedPageable); - Page uiPage = page.map(mapper::toVo); - - return ResponseEntity.ok(new GridResponse<>(uiPage)); + Page page = service.selectList(search, pageable); + return ResponseEntity.ok(new GridResponse<>(page)); } - @PostMapping(value = "/onl/djb/statistics/apiStatsDayMan.json", params = "cmd=EXCEL_EXPORT") + @PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.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()); + List uiList = service.selectList(search); + log.info("Data retrieved - count: {}", uiList.size()); - if (dataList.isEmpty()) { + if (uiList.isEmpty()) { log.warn("No data found for export"); response.setStatus(HttpServletResponse.SC_NO_CONTENT); response.setContentType("application/json; charset=UTF-8"); @@ -74,11 +59,6 @@ public class ApiUseStatsController { 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); @@ -101,10 +81,7 @@ public class ApiUseStatsController { } private String generateFileName(ApiStatsSearch search) { - String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime()) - ? search.getSearchStartDateTime() - : LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - return "API사용현황_" + timeRange + ".xlsx"; + return "API사용현황_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx"; } } diff --git a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/StandardMessageInfoQueryServiceForApi.java b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/StandardMessageInfoQueryServiceForApi.java index af98df0..4f97bfd 100644 --- a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/StandardMessageInfoQueryServiceForApi.java +++ b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/StandardMessageInfoQueryServiceForApi.java @@ -19,7 +19,7 @@ public class StandardMessageInfoQueryServiceForApi extends AbstractDataService { Optional findByEaisvcname(String eaiSvcName); - @Modifying - @Query("DELETE FROM StandardMessageInfo s WHERE s.eaisvcname = :eaiSvcName") - void deleteByEaisvcname(@Param("eaiSvcName") String eaiSvcName); +// @Modifying +// @Query("DELETE FROM StandardMessageInfo s WHERE s.eaisvcname = :eaiSvcName") +// void deleteByEaisvcname(@Param("eaiSvcName") String eaiSvcName); boolean existsByApifullpathAndEaisvcnameNot(String fullPath, String eaiServiceName); } diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsAggregationController.java b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsAggregationController.java index a8b1df0..6d19e66 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsAggregationController.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsAggregationController.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; +import com.eactive.eai.rms.ext.djb.job.ApiStatsHourlyAggregationJob; import com.eactive.ext.kjb.statistics.job.DailyToMonthlyAggregationJob; import com.eactive.ext.kjb.statistics.job.HourlyToDailyAggregationJob; import com.eactive.ext.kjb.statistics.job.MonthlyToYearlyAggregationJob; @@ -33,12 +34,62 @@ public class ApiStatsAggregationController { private final HourlyToDailyAggregationJob hourlyToDailyAggregationJob; private final DailyToMonthlyAggregationJob dailyToMonthlyAggregationJob; private final MonthlyToYearlyAggregationJob monthlyToYearlyAggregationJob; + + private final ApiStatsHourlyAggregationJob apiStatsHourlyAggregationJob; @GetMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.view") public String view() { return "/onl/kjb/statistics/apiStatsAggregationMan"; } + /** + * 거래로그→시간별 통계 집계 수동 실행 + * @param targetDate 집계 대상 날짜 (yyyyMMdd 형식, 예: 20250120) + * @return 처리 결과 + */ + @PostMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.json", params = "cmd=AGGREGATION_HOUR") + public ResponseEntity> executeAggregationHourly( + @RequestParam(required = false) String targetDate) { + + Map result = new HashMap<>(); + + try { + // 날짜 입력 검증 + if (targetDate == null || targetDate.trim().isEmpty()) { + log.error("targetDate 미입력"); + result.put("success", false); + result.put("message", "대상 날짜를 입력하세요. (예: 20250120)"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); + } + + LocalDate date; + try { + date = LocalDate.parse(targetDate, DateTimeFormatter.ofPattern("yyyyMMdd")); + } catch (DateTimeParseException e) { + log.error("잘못된 날짜 형식: {}", targetDate); + result.put("success", false); + result.put("message", "잘못된 날짜 형식입니다. yyyyMMdd 형식으로 입력하세요. (예: 20250120)"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); + } + + log.info("거래로그→시간별 통계 집계 수동 실행 시작. targetDate: {}", date); + int count = apiStatsHourlyAggregationJob.executeManual(date); + + result.put("success", true); + result.put("message", "거래로그→시간별 통계 집계가 완료되었습니다."); + result.put("targetDate", date.format(DateTimeFormatter.ofPattern("yyyyMMdd"))); + result.put("processedCount", count); + + return ResponseEntity.ok(result); + + } catch (Exception e) { + log.error("거래로그→시간별 통계 집계 실행 중 오류 발생", e); + result.put("success", false); + result.put("message", "집계 실행 중 오류가 발생했습니다: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); + } + } + /** * 시간별→일별 통계 집계 수동 실행 * @param targetDate 집계 대상 날짜 (yyyyMMdd 형식, 예: 20250120) diff --git a/src/main/java/com/eactive/ext/kjb/statistics/job/DailyToMonthlyAggregationJob.java b/src/main/java/com/eactive/ext/kjb/statistics/job/DailyToMonthlyAggregationJob.java index cb25000..c6e56ba 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/job/DailyToMonthlyAggregationJob.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/job/DailyToMonthlyAggregationJob.java @@ -138,9 +138,9 @@ public class DailyToMonthlyAggregationJob implements Job { qd.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"), qd.seq900BizErrCnt.sum().as("seq900BizErrCnt"), Expressions.cases() - .when(qd.successCnt.sum().gt(0)) - .then(qd.avgRespTime.multiply(qd.successCnt).sum() - .divide(qd.successCnt.sum()).castToNum(BigDecimal.class)) + .when(qd.totalCnt.sum().gt(0)) + .then(qd.avgRespTime.multiply(qd.totalCnt).sum() + .divide(qd.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"), qd.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), diff --git a/src/main/java/com/eactive/ext/kjb/statistics/job/HourlyToDailyAggregationJob.java b/src/main/java/com/eactive/ext/kjb/statistics/job/HourlyToDailyAggregationJob.java index a71b783..97729b7 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/job/HourlyToDailyAggregationJob.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/job/HourlyToDailyAggregationJob.java @@ -133,9 +133,9 @@ public class HourlyToDailyAggregationJob implements Job { qh.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"), qh.seq900BizErrCnt.sum().as("seq900BizErrCnt"), Expressions.cases() - .when(qh.successCnt.sum().gt(0)) - .then(qh.avgRespTime.multiply(qh.successCnt).sum() - .divide(qh.successCnt.sum()).castToNum(BigDecimal.class)) + .when(qh.totalCnt.sum().gt(0)) + .then(qh.avgRespTime.multiply(qh.totalCnt).sum() + .divide(qh.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"), qh.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), diff --git a/src/main/java/com/eactive/ext/kjb/statistics/job/MonthlyToYearlyAggregationJob.java b/src/main/java/com/eactive/ext/kjb/statistics/job/MonthlyToYearlyAggregationJob.java index c3fe895..a7ebf6c 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/job/MonthlyToYearlyAggregationJob.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/job/MonthlyToYearlyAggregationJob.java @@ -136,9 +136,9 @@ public class MonthlyToYearlyAggregationJob implements Job { qm.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"), qm.seq900BizErrCnt.sum().as("seq900BizErrCnt"), Expressions.cases() - .when(qm.successCnt.sum().gt(0)) - .then(qm.avgRespTime.multiply(qm.successCnt).sum() - .divide(qm.successCnt.sum()).castToNum(BigDecimal.class)) + .when(qm.totalCnt.sum().gt(0)) + .then(qm.avgRespTime.multiply(qm.totalCnt).sum() + .divide(qm.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"), qm.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), 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 index a97c2fe..ec92563 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/service/ApiStatsExcelExportService.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/service/ApiStatsExcelExportService.java @@ -36,9 +36,8 @@ public class ApiStatsExcelExportService { private static final String[] COLUMN_HEADERS = { "통계시간", "API명", "인스턴스", "업무구분", "클라이언트ID", "Inbound Adapter", "Outbound Adapter", - "총건수", "성공건수", "Timeout건수", "시스템오류건수", "업무오류건수", - "Seq900 Timeout", "Seq900 시스템오류", "Seq900 업무오류", - "평균응답시간", "최소응답시간", "최대응답시간", "P50응답시간", "P95응답시간" + "총건수", "성공건수", "Timeout건수", "시스템오류건수", + "평균응답시간", "최소응답시간", "최대응답시간" }; /** @@ -193,17 +192,13 @@ public class ApiStatsExcelExportService { 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); + } /** diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java index 96c0e58..2f354fd 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java @@ -18,6 +18,8 @@ public class ApiStatsSearch { private String searchClientId; private String searchInboundAdapter; private String searchOutboundAdapter; + private String searchOrgName; + private String searchType; /** 파싱된 시작시간 */ private LocalDateTime parsedStartDateTime; diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java index 19e0368..83ee405 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java @@ -14,6 +14,7 @@ public class ApiStatsUI { private String gwInstanceId; private String bizDivCode; private String clientId; + private String orgName; private String inboundAdapter; private String outboundAdapter; @@ -22,15 +23,13 @@ public class ApiStatsUI { private Long successCnt; private Long timeoutCnt; private Long systemErrCnt; - private Long bizErrCnt; - private Long seq900TimeoutCnt; - private Long seq900SystemErrCnt; - private Long seq900BizErrCnt; // 응답시간 메트릭 private BigDecimal avgRespTime; private BigDecimal minRespTime; private BigDecimal maxRespTime; - private BigDecimal p50RespTime; - private BigDecimal p95RespTime; + + //성공율,실패율 + private BigDecimal successRate; + private BigDecimal failRate; }