diff --git a/WebContent/WEB-INF/springapp-servlet.xml b/WebContent/WEB-INF/springapp-servlet.xml index 3bef9a1..89c338a 100644 --- a/WebContent/WEB-INF/springapp-servlet.xml +++ b/WebContent/WEB-INF/springapp-servlet.xml @@ -152,4 +152,9 @@ + + + + + diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp new file mode 100644 index 0000000..8322d71 --- /dev/null +++ b/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp @@ -0,0 +1,353 @@ +<%@ page language="java" contentType="text/html; charset=utf-8"%> +<%@ page import="java.io.*"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ include file="/jsp/common/include/localemessage.jsp" %> +<% + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", "0"); + request.setCharacterEncoding("UTF-8"); +%> + + + API 통계 집계 실행 + + + + + + + +
+
+ +
+
+
API 통계 집계 수동 실행
+ +
+ ⚠️ 주의사항 +
    +
  • 통계 집계는 스케줄러에 의해 자동으로 실행됩니다.
  • +
  • 수동 실행은 누락된 기간이나 재집계가 필요한 경우에만 사용하세요.
  • +
  • 집계 대상 기간의 원본 데이터가 존재해야 합니다.
  • +
  • 대상 날짜/월/연도를 반드시 입력해야 합니다.
  • +
+
+ + +
+

1. 시간별 → 일별 통계 집계

+
+ + + (예: 20250120) + +
+
+ 실행 결과: +

+			
+
+ + +
+

2. 일별 → 월별 통계 집계

+
+ + + (예: 202501) + +
+
+ 실행 결과: +

+			
+
+ + +
+

3. 월별 → 연별 통계 집계

+
+ + + (예: 2024) + +
+
+ 실행 결과: +

+			
+
+
+
+ + diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsDayMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsDayMan.jsp new file mode 100644 index 0000000..ee499a4 --- /dev/null +++ b/WebContent/jsp/onl/kjb/statistics/apiStatsDayMan.jsp @@ -0,0 +1,695 @@ +<%@ page language="java" contentType="text/html; charset=utf-8"%> +<%@ page import="java.io.*"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ include file="/jsp/common/include/localemessage.jsp" %> +<% + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", "0"); + request.setCharacterEncoding("UTF-8"); +%> + + + 일별 통계 조회 + + + + + + + + +
+
+ +
+
+
+ + +
+
API 일별 통계
+ + + + + + + + + + + + + + + + + + + + + + + + + +
조회기간 + + ~ + + (최대 31일) +
API명 + + 인스턴스 + +
업무구분 + + 클라이언트ID + +
Inbound Adapter + + Outbound Adapter + +
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ + diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsMonthMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsMonthMan.jsp new file mode 100644 index 0000000..7c837fc --- /dev/null +++ b/WebContent/jsp/onl/kjb/statistics/apiStatsMonthMan.jsp @@ -0,0 +1,702 @@ +<%@ page language="java" contentType="text/html; charset=utf-8"%> +<%@ page import="java.io.*"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ include file="/jsp/common/include/localemessage.jsp" %> +<% + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", "0"); + request.setCharacterEncoding("UTF-8"); +%> + + + 월별 통계 조회 + + + + + + + + +
+
+ +
+
+
+ + +
+
API 월별 통계
+ + + + + + + + + + + + + + + + + + + + + + + + + +
조회기간 + + ~ + + (최대 24개월) +
API명 + + 인스턴스 + +
업무구분 + + 클라이언트ID + +
Inbound Adapter + + Outbound Adapter + +
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ + diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsYearMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsYearMan.jsp new file mode 100644 index 0000000..7e01a46 --- /dev/null +++ b/WebContent/jsp/onl/kjb/statistics/apiStatsYearMan.jsp @@ -0,0 +1,670 @@ +<%@ page language="java" contentType="text/html; charset=utf-8"%> +<%@ page import="java.io.*"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ include file="/jsp/common/include/localemessage.jsp" %> +<% + response.setHeader("Pragma", "No-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Expires", "0"); + request.setCharacterEncoding("UTF-8"); +%> + + + 연간 통계 조회 + + + + + + + + +
+
+ +
+
+
+ + +
+
API 연간 통계
+ + + + + + + + + + + + + + + + + + + + + + + + + +
조회기간 + + ~ + +
API명 + + 인스턴스 + +
업무구분 + + 클라이언트ID + +
Inbound Adapter + + Outbound Adapter + +
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ + diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDay.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDay.java new file mode 100644 index 0000000..28e7d4f --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDay.java @@ -0,0 +1,93 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.Table; + +import lombok.Getter; +import lombok.Setter; + +/** + * API Gateway 일별 처리 통계 + */ +@Getter +@Setter +@Entity +@Table(name = "API_STATS_DAY") +@IdClass(ApiStatsDayId.class) +public class ApiStatsDay implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "STAT_TIME", nullable = false) + private LocalDate statTime; + + @Id + @Column(name = "API_NAME", nullable = false, length = 100) + private String apiName; + + @Id + @Column(name = "GW_INSTANCE_ID", nullable = false, length = 50) + private String gwInstanceId; + + @Id + @Column(name = "BIZ_DIV_CODE", nullable = false, length = 50) + private String bizDivCode = "NONE"; + + @Id + @Column(name = "CLIENT_ID", nullable = false, length = 100) + private String clientId = "NONE"; + + @Id + @Column(name = "INBOUND_ADAPTER", nullable = false, length = 100) + private String inboundAdapter = "NONE"; + + @Id + @Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100) + private String outboundAdapter = "NONE"; + + @Column(name = "TOTAL_CNT", nullable = false) + private Long totalCnt = 0L; + + @Column(name = "SUCCESS_CNT", nullable = false) + private Long successCnt = 0L; + + @Column(name = "TIMEOUT_CNT", nullable = false) + private Long timeoutCnt = 0L; + + @Column(name = "SYSTEM_ERR_CNT", nullable = false) + private Long systemErrCnt = 0L; + + @Column(name = "BIZ_ERR_CNT", nullable = false) + private Long bizErrCnt = 0L; + + @Column(name = "SEQ900_TIMEOUT_CNT", nullable = false) + private Long seq900TimeoutCnt = 0L; + + @Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false) + private Long seq900SystemErrCnt = 0L; + + @Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false) + private Long seq900BizErrCnt = 0L; + + @Column(name = "AVG_RESP_TIME", precision = 10, scale = 3) + private BigDecimal avgRespTime = BigDecimal.ZERO; + + @Column(name = "MIN_RESP_TIME", precision = 10, scale = 3) + private BigDecimal minRespTime = BigDecimal.ZERO; + + @Column(name = "MAX_RESP_TIME", precision = 10, scale = 3) + private BigDecimal maxRespTime = BigDecimal.ZERO; + + @Column(name = "P50_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p50RespTime = BigDecimal.ZERO; + + @Column(name = "P95_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p95RespTime = BigDecimal.ZERO; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayId.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayId.java new file mode 100644 index 0000000..8b50d29 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayId.java @@ -0,0 +1,26 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.io.Serializable; +import java.time.LocalDate; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * API 통계 일별 복합키 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ApiStatsDayId implements Serializable { + private static final long serialVersionUID = 1L; + + private LocalDate statTime; + private String apiName; + private String gwInstanceId; + private String bizDivCode; + private String clientId; + private String inboundAdapter; + private String outboundAdapter; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayRepository.java new file mode 100644 index 0000000..206215a --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayRepository.java @@ -0,0 +1,10 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import com.eactive.eai.data.jpa.BaseRepository; + +/** + * API 통계 일별 Repository + */ +interface ApiStatsDayRepository extends BaseRepository { + +} 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 new file mode 100644 index 0000000..251b1b4 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayService.java @@ -0,0 +1,152 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +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.ext.kjb.statistics.ui.ApiStatsChartUI; +import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; + +@Service +@Transactional +public class ApiStatsDayService + extends AbstractDataService { + + private static final int MAX_DAYS = 31; + + public Page selectList(ApiStatsSearch search, Pageable pageable) { + QApiStatsDay q = QApiStatsDay.apiStatsDay; + BooleanBuilder builder = buildSearchConditions(q, search); + return repository.findAll(builder, pageable); + } + + public List selectListForExcel(ApiStatsSearch search) { + QApiStatsDay q = QApiStatsDay.apiStatsDay; + BooleanBuilder builder = buildSearchConditions(q, search); + + return getJPAQueryFactory() + .selectFrom(q) + .where(builder) + .orderBy(q.statTime.desc()) + .fetch(); + } + + public List selectChartData(ApiStatsSearch search) { + QApiStatsDay q = QApiStatsDay.apiStatsDay; + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartDate() != null) { + builder.and(q.statTime.goe(search.getParsedStartDate())); + builder.and(q.statTime.loe(search.getParsedEndDate())); + } + + 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())); + } + + return getJPAQueryFactory() + .select(Projections.constructor(ApiStatsChartUI.class, + q.statTime, + q.totalCnt.sum(), + q.successCnt.sum(), + q.timeoutCnt.sum(), + q.systemErrCnt.sum(), + q.bizErrCnt.sum(), + q.seq900TimeoutCnt.sum(), + q.seq900SystemErrCnt.sum(), + q.seq900BizErrCnt.sum(), + q.avgRespTime.avg(), + q.p50RespTime.avg(), + q.p95RespTime.avg())) + .from(q) + .where(builder) + .groupBy(q.statTime) + .orderBy(q.statTime.asc()) + .fetch(); + } + + private BooleanBuilder buildSearchConditions(QApiStatsDay q, ApiStatsSearch search) { + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartDate() != null) { + builder.and(q.statTime.goe(search.getParsedStartDate())); + builder.and(q.statTime.loe(search.getParsedEndDate())); + } + + 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 builder; + } + + /** + * 조회 기간 계산 (최대 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")); + // 31일 초과 시 시작날짜 기준 31일 후로 강제 설정 + 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/data/entity/onl/kjb/statistics/ApiStatsMonth.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonth.java new file mode 100644 index 0000000..944595d --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonth.java @@ -0,0 +1,92 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.io.Serializable; +import java.math.BigDecimal; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.Table; + +import lombok.Getter; +import lombok.Setter; + +/** + * API Gateway 월별 처리 통계 + */ +@Getter +@Setter +@Entity +@Table(name = "API_STATS_MONTH") +@IdClass(ApiStatsMonthId.class) +public class ApiStatsMonth implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "STAT_TIME", nullable = false, length = 6) + private String statTime; // YYYYMM format + + @Id + @Column(name = "API_NAME", nullable = false, length = 100) + private String apiName; + + @Id + @Column(name = "GW_INSTANCE_ID", nullable = false, length = 50) + private String gwInstanceId; + + @Id + @Column(name = "BIZ_DIV_CODE", nullable = false, length = 50) + private String bizDivCode = "NONE"; + + @Id + @Column(name = "CLIENT_ID", nullable = false, length = 100) + private String clientId = "NONE"; + + @Id + @Column(name = "INBOUND_ADAPTER", nullable = false, length = 100) + private String inboundAdapter = "NONE"; + + @Id + @Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100) + private String outboundAdapter = "NONE"; + + @Column(name = "TOTAL_CNT", nullable = false) + private Long totalCnt = 0L; + + @Column(name = "SUCCESS_CNT", nullable = false) + private Long successCnt = 0L; + + @Column(name = "TIMEOUT_CNT", nullable = false) + private Long timeoutCnt = 0L; + + @Column(name = "SYSTEM_ERR_CNT", nullable = false) + private Long systemErrCnt = 0L; + + @Column(name = "BIZ_ERR_CNT", nullable = false) + private Long bizErrCnt = 0L; + + @Column(name = "SEQ900_TIMEOUT_CNT", nullable = false) + private Long seq900TimeoutCnt = 0L; + + @Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false) + private Long seq900SystemErrCnt = 0L; + + @Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false) + private Long seq900BizErrCnt = 0L; + + @Column(name = "AVG_RESP_TIME", precision = 10, scale = 3) + private BigDecimal avgRespTime = BigDecimal.ZERO; + + @Column(name = "MIN_RESP_TIME", precision = 10, scale = 3) + private BigDecimal minRespTime = BigDecimal.ZERO; + + @Column(name = "MAX_RESP_TIME", precision = 10, scale = 3) + private BigDecimal maxRespTime = BigDecimal.ZERO; + + @Column(name = "P50_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p50RespTime = BigDecimal.ZERO; + + @Column(name = "P95_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p95RespTime = BigDecimal.ZERO; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthId.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthId.java new file mode 100644 index 0000000..91366ec --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthId.java @@ -0,0 +1,25 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.io.Serializable; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * API 통계 월별 복합키 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ApiStatsMonthId implements Serializable { + private static final long serialVersionUID = 1L; + + private String statTime; // YYYYMM format + private String apiName; + private String gwInstanceId; + private String bizDivCode; + private String clientId; + private String inboundAdapter; + private String outboundAdapter; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthRepository.java new file mode 100644 index 0000000..6942bde --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthRepository.java @@ -0,0 +1,10 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import com.eactive.eai.data.jpa.BaseRepository; + +/** + * API 통계 월별 Repository + */ +interface ApiStatsMonthRepository extends BaseRepository { + +} 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 new file mode 100644 index 0000000..928ce4a --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthService.java @@ -0,0 +1,153 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +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.ext.kjb.statistics.ui.ApiStatsChartUI; +import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; + +@Service +@Transactional +public class ApiStatsMonthService + extends AbstractDataService { + + private static final int MAX_MONTHS = 24; + + public Page selectList(ApiStatsSearch search, Pageable pageable) { + QApiStatsMonth q = QApiStatsMonth.apiStatsMonth; + BooleanBuilder builder = buildSearchConditions(q, search); + return repository.findAll(builder, pageable); + } + + public List selectListForExcel(ApiStatsSearch search) { + QApiStatsMonth q = QApiStatsMonth.apiStatsMonth; + BooleanBuilder builder = buildSearchConditions(q, search); + + return getJPAQueryFactory() + .selectFrom(q) + .where(builder) + .orderBy(q.statTime.desc()) + .fetch(); + } + + public List selectChartData(ApiStatsSearch search) { + QApiStatsMonth q = QApiStatsMonth.apiStatsMonth; + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartMonth() != null) { + builder.and(q.statTime.goe(search.getParsedStartMonth())); + builder.and(q.statTime.loe(search.getParsedEndMonth())); + } + + 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())); + } + + return getJPAQueryFactory() + .select(Projections.constructor(ApiStatsChartUI.class, + q.statTime, + q.totalCnt.sum(), + q.successCnt.sum(), + q.timeoutCnt.sum(), + q.systemErrCnt.sum(), + q.bizErrCnt.sum(), + q.seq900TimeoutCnt.sum(), + q.seq900SystemErrCnt.sum(), + q.seq900BizErrCnt.sum(), + q.avgRespTime.avg(), + q.p50RespTime.avg(), + q.p95RespTime.avg())) + .from(q) + .where(builder) + .groupBy(q.statTime) + .orderBy(q.statTime.asc()) + .fetch(); + } + + private BooleanBuilder buildSearchConditions(QApiStatsMonth q, ApiStatsSearch search) { + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartMonth() != null) { + builder.and(q.statTime.goe(search.getParsedStartMonth())); + builder.and(q.statTime.loe(search.getParsedEndMonth())); + } + + 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 builder; + } + + /** + * 조회 기간 계산 (최대 24개월 제한) + */ + private void calculateDateRange(ApiStatsSearch search) { + if (StringUtils.isBlank(search.getSearchStartDateTime())) { + return; + } + + YearMonth startMonth = YearMonth.parse(search.getSearchStartDateTime(), + DateTimeFormatter.ofPattern("yyyyMM")); + YearMonth endMonth; + + if (StringUtils.isNotBlank(search.getSearchEndDateTime())) { + endMonth = YearMonth.parse(search.getSearchEndDateTime(), + DateTimeFormatter.ofPattern("yyyyMM")); + // 12개월 초과 시 시작월 기준 12개월 후로 강제 설정 + long monthsBetween = java.time.temporal.ChronoUnit.MONTHS.between(startMonth, endMonth); + if (monthsBetween > MAX_MONTHS) { + endMonth = startMonth.plusMonths(MAX_MONTHS); + } + } else { + endMonth = startMonth.plusMonths(MAX_MONTHS); + } + + search.setParsedStartMonth(startMonth.format(DateTimeFormatter.ofPattern("yyyyMM"))); + search.setParsedEndMonth(endMonth.format(DateTimeFormatter.ofPattern("yyyyMM"))); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYear.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYear.java new file mode 100644 index 0000000..bd5f6ba --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYear.java @@ -0,0 +1,92 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.io.Serializable; +import java.math.BigDecimal; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.Table; + +import lombok.Getter; +import lombok.Setter; + +/** + * API Gateway 연간 처리 통계 + */ +@Getter +@Setter +@Entity +@Table(name = "API_STATS_YEAR") +@IdClass(ApiStatsYearId.class) +public class ApiStatsYear implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "STAT_TIME", nullable = false, length = 4) + private String statTime; // YYYY format + + @Id + @Column(name = "API_NAME", nullable = false, length = 100) + private String apiName; + + @Id + @Column(name = "GW_INSTANCE_ID", nullable = false, length = 50) + private String gwInstanceId; + + @Id + @Column(name = "BIZ_DIV_CODE", nullable = false, length = 50) + private String bizDivCode = "NONE"; + + @Id + @Column(name = "CLIENT_ID", nullable = false, length = 100) + private String clientId = "NONE"; + + @Id + @Column(name = "INBOUND_ADAPTER", nullable = false, length = 100) + private String inboundAdapter = "NONE"; + + @Id + @Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100) + private String outboundAdapter = "NONE"; + + @Column(name = "TOTAL_CNT", nullable = false) + private Long totalCnt = 0L; + + @Column(name = "SUCCESS_CNT", nullable = false) + private Long successCnt = 0L; + + @Column(name = "TIMEOUT_CNT", nullable = false) + private Long timeoutCnt = 0L; + + @Column(name = "SYSTEM_ERR_CNT", nullable = false) + private Long systemErrCnt = 0L; + + @Column(name = "BIZ_ERR_CNT", nullable = false) + private Long bizErrCnt = 0L; + + @Column(name = "SEQ900_TIMEOUT_CNT", nullable = false) + private Long seq900TimeoutCnt = 0L; + + @Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false) + private Long seq900SystemErrCnt = 0L; + + @Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false) + private Long seq900BizErrCnt = 0L; + + @Column(name = "AVG_RESP_TIME", precision = 10, scale = 3) + private BigDecimal avgRespTime = BigDecimal.ZERO; + + @Column(name = "MIN_RESP_TIME", precision = 10, scale = 3) + private BigDecimal minRespTime = BigDecimal.ZERO; + + @Column(name = "MAX_RESP_TIME", precision = 10, scale = 3) + private BigDecimal maxRespTime = BigDecimal.ZERO; + + @Column(name = "P50_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p50RespTime = BigDecimal.ZERO; + + @Column(name = "P95_RESP_TIME", precision = 10, scale = 3) + private BigDecimal p95RespTime = BigDecimal.ZERO; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearId.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearId.java new file mode 100644 index 0000000..a37a29f --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearId.java @@ -0,0 +1,25 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.io.Serializable; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * API 통계 연간 복합키 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ApiStatsYearId implements Serializable { + private static final long serialVersionUID = 1L; + + private String statTime; // YYYY format + private String apiName; + private String gwInstanceId; + private String bizDivCode; + private String clientId; + private String inboundAdapter; + private String outboundAdapter; +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearRepository.java new file mode 100644 index 0000000..5961d89 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearRepository.java @@ -0,0 +1,10 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import com.eactive.eai.data.jpa.BaseRepository; + +/** + * API 통계 연간 Repository + */ +interface ApiStatsYearRepository extends BaseRepository { + +} 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 new file mode 100644 index 0000000..64145eb --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearService.java @@ -0,0 +1,143 @@ +package com.eactive.eai.rms.data.entity.onl.kjb.statistics; + +import java.time.Year; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +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.ext.kjb.statistics.ui.ApiStatsChartUI; +import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; + +@Service +@Transactional +public class ApiStatsYearService + extends AbstractDataService { + + public Page selectList(ApiStatsSearch search, Pageable pageable) { + QApiStatsYear q = QApiStatsYear.apiStatsYear; + BooleanBuilder builder = buildSearchConditions(q, search); + return repository.findAll(builder, pageable); + } + + public List selectListForExcel(ApiStatsSearch search) { + QApiStatsYear q = QApiStatsYear.apiStatsYear; + BooleanBuilder builder = buildSearchConditions(q, search); + + return getJPAQueryFactory() + .selectFrom(q) + .where(builder) + .orderBy(q.statTime.desc()) + .fetch(); + } + + public List selectChartData(ApiStatsSearch search) { + QApiStatsYear q = QApiStatsYear.apiStatsYear; + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartYear() != null) { + builder.and(q.statTime.goe(search.getParsedStartYear())); + builder.and(q.statTime.loe(search.getParsedEndYear())); + } + + 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())); + } + + return getJPAQueryFactory() + .select(Projections.constructor(ApiStatsChartUI.class, + q.statTime, + q.totalCnt.sum(), + q.successCnt.sum(), + q.timeoutCnt.sum(), + q.systemErrCnt.sum(), + q.bizErrCnt.sum(), + q.seq900TimeoutCnt.sum(), + q.seq900SystemErrCnt.sum(), + q.seq900BizErrCnt.sum(), + q.avgRespTime.avg(), + q.p50RespTime.avg(), + q.p95RespTime.avg())) + .from(q) + .where(builder) + .groupBy(q.statTime) + .orderBy(q.statTime.asc()) + .fetch(); + } + + private BooleanBuilder buildSearchConditions(QApiStatsYear q, ApiStatsSearch search) { + BooleanBuilder builder = new BooleanBuilder(); + + calculateDateRange(search); + if (search.getParsedStartYear() != null) { + builder.and(q.statTime.goe(search.getParsedStartYear())); + builder.and(q.statTime.loe(search.getParsedEndYear())); + } + + 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 builder; + } + + /** + * 조회 기간 계산 + */ + private void calculateDateRange(ApiStatsSearch search) { + if (StringUtils.isBlank(search.getSearchStartDateTime())) { + return; + } + + Year startYear = Year.parse(search.getSearchStartDateTime()); + Year endYear; + + if (StringUtils.isNotBlank(search.getSearchEndDateTime())) { + endYear = Year.parse(search.getSearchEndDateTime()); + } else { + endYear = Year.now(); + } + + search.setParsedStartYear(String.valueOf(startYear.getValue())); + search.setParsedEndYear(String.valueOf(endYear.getValue())); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsAggregationController.java b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsAggregationController.java new file mode 100644 index 0000000..a8b1df0 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsAggregationController.java @@ -0,0 +1,185 @@ +package com.eactive.ext.kjb.statistics; + +import java.time.LocalDate; +import java.time.Year; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.HttpStatus; +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 org.springframework.web.bind.annotation.RequestParam; + +import com.eactive.ext.kjb.statistics.job.DailyToMonthlyAggregationJob; +import com.eactive.ext.kjb.statistics.job.HourlyToDailyAggregationJob; +import com.eactive.ext.kjb.statistics.job.MonthlyToYearlyAggregationJob; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * API 통계 집계 수동 실행 Controller + */ +@Slf4j +@Controller +@RequiredArgsConstructor +public class ApiStatsAggregationController { + + private final HourlyToDailyAggregationJob hourlyToDailyAggregationJob; + private final DailyToMonthlyAggregationJob dailyToMonthlyAggregationJob; + private final MonthlyToYearlyAggregationJob monthlyToYearlyAggregationJob; + + @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_DAY") + public ResponseEntity> executeHourlyToDaily( + @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 = hourlyToDailyAggregationJob.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 targetMonth 집계 대상 월 (yyyyMM 형식, 예: 202501) + * @return 처리 결과 + */ + @PostMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.json", params = "cmd=AGGREGATION_MONTH") + public ResponseEntity> executeDailyToMonthly( + @RequestParam(required = false) String targetMonth) { + + Map result = new HashMap<>(); + + try { + // 월 입력 검증 + if (targetMonth == null || targetMonth.trim().isEmpty()) { + log.error("targetMonth 미입력"); + result.put("success", false); + result.put("message", "대상 월을 입력하세요. (예: 202501)"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); + } + + YearMonth month; + try { + month = YearMonth.parse(targetMonth, DateTimeFormatter.ofPattern("yyyyMM")); + } catch (DateTimeParseException e) { + log.error("잘못된 월 형식: {}", targetMonth); + result.put("success", false); + result.put("message", "잘못된 월 형식입니다. yyyyMM 형식으로 입력하세요. (예: 202501)"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); + } + + log.info("일별→월별 통계 집계 수동 실행 시작. targetMonth: {}", month); + int count = dailyToMonthlyAggregationJob.executeManual(month); + + result.put("success", true); + result.put("message", "일별→월별 통계 집계가 완료되었습니다."); + result.put("targetMonth", month.format(DateTimeFormatter.ofPattern("yyyyMM"))); + 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 targetYear 집계 대상 연도 (yyyy 형식, 예: 2024) + * @return 처리 결과 + */ + @PostMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.json", params = "cmd=AGGREGATION_YEAR") + public ResponseEntity> executeMonthlyToYearly( + @RequestParam(required = false) String targetYear) { + + Map result = new HashMap<>(); + + try { + // 연도 입력 검증 + if (targetYear == null || targetYear.trim().isEmpty()) { + log.error("targetYear 미입력"); + result.put("success", false); + result.put("message", "대상 연도를 입력하세요. (예: 2024)"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); + } + + Year year; + try { + year = Year.parse(targetYear); + } catch (DateTimeParseException e) { + log.error("잘못된 연도 형식: {}", targetYear); + result.put("success", false); + result.put("message", "잘못된 연도 형식입니다. yyyy 형식으로 입력하세요. (예: 2024)"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); + } + + log.info("월별→연별 통계 집계 수동 실행 시작. targetYear: {}", year); + int count = monthlyToYearlyAggregationJob.executeManual(year); + + result.put("success", true); + result.put("message", "월별→연별 통계 집계가 완료되었습니다."); + result.put("targetYear", year.toString()); + 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); + } + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsDayController.java b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsDayController.java new file mode 100644 index 0000000..3221419 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsDayController.java @@ -0,0 +1,115 @@ +package com.eactive.ext.kjb.statistics; + +import java.io.IOException; +import java.time.LocalDate; +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.ext.kjb.statistics.ui.ApiStatsSearch; +import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class ApiStatsDayController { + + private final ApiStatsDayService service; + private final ApiStatsUIMapper mapper; + private final ApiStatsExcelExportService excelExportService; + + @GetMapping(value = "/onl/kjb/statistics/apiStatsDayMan.view") + public String view() { + return "/onl/kjb/statistics/apiStatsDayMan"; + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.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)); + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.json", params = "cmd=CHART") + public ResponseEntity> selectChartData(ApiStatsSearch search) { + List chartData = service.selectChartData(search); + return ResponseEntity.ok(chartData); + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.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() + : LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + return "API일별통계_" + timeRange + ".xlsx"; + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMonthController.java b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMonthController.java new file mode 100644 index 0000000..55e474e --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsMonthController.java @@ -0,0 +1,115 @@ +package com.eactive.ext.kjb.statistics; + +import java.io.IOException; +import java.time.YearMonth; +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.ApiStatsMonth; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonthService; +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; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class ApiStatsMonthController { + + private final ApiStatsMonthService service; + private final ApiStatsUIMapper mapper; + private final ApiStatsExcelExportService excelExportService; + + @GetMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.view") + public String view() { + return "/onl/kjb/statistics/apiStatsMonthMan"; + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.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)); + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.json", params = "cmd=CHART") + public ResponseEntity> selectChartData(ApiStatsSearch search) { + List chartData = service.selectChartData(search); + return ResponseEntity.ok(chartData); + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.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() + : YearMonth.now().format(DateTimeFormatter.ofPattern("yyyyMM")); + return "API월별통계_" + timeRange + ".xlsx"; + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsYearController.java b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsYearController.java new file mode 100644 index 0000000..c2e587a --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/ApiStatsYearController.java @@ -0,0 +1,114 @@ +package com.eactive.ext.kjb.statistics; + +import java.io.IOException; +import java.time.Year; +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.ApiStatsYear; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYearService; +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; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class ApiStatsYearController { + + private final ApiStatsYearService service; + private final ApiStatsUIMapper mapper; + private final ApiStatsExcelExportService excelExportService; + + @GetMapping(value = "/onl/kjb/statistics/apiStatsYearMan.view") + public String view() { + return "/onl/kjb/statistics/apiStatsYearMan"; + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.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)); + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.json", params = "cmd=CHART") + public ResponseEntity> selectChartData(ApiStatsSearch search) { + List chartData = service.selectChartData(search); + return ResponseEntity.ok(chartData); + } + + @PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.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() + : String.valueOf(Year.now().getValue()); + return "API연간통계_" + timeRange + ".xlsx"; + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/README.md b/src/main/java/com/eactive/ext/kjb/statistics/README.md deleted file mode 100644 index 3a93ba4..0000000 --- a/src/main/java/com/eactive/ext/kjb/statistics/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# API Gateway 통계 화면 - -## 개요 - -API Gateway의 분단위/시간별 처리 통계를 조회하는 관리 화면입니다. - -## 파일 구조 - -``` -com.eactive.ext.kjb.statistics/ -├── ApiStatsMinuteController.java # 분단위 통계 Controller -├── ApiStatsMinuteService.java # 분단위 통계 Service -├── ApiStatsHourController.java # 시간별 통계 Controller -├── ApiStatsHourService.java # 시간별 통계 Service -├── entity/ -│ ├── ApiStatsMinute.java # 분단위 통계 Entity -│ ├── ApiStatsMinuteId.java # 분단위 통계 복합키 -│ ├── ApiStatsHour.java # 시간별 통계 Entity -│ └── ApiStatsHourId.java # 시간별 통계 복합키 -├── repository/ -│ ├── ApiStatsMinuteRepository.java -│ └── ApiStatsHourRepository.java -├── ui/ -│ ├── ApiStatsUI.java # 통계 UI DTO -│ └── ApiStatsSearch.java # 검색조건 DTO -├── mapping/ -│ └── ApiStatsUIMapper.java # Entity ↔ UI 매핑 -└── README.md - -JSP 화면: -WebContent/jsp/kjb/statistics/ -├── apiStatsMinuteMan.jsp # 분단위 통계 화면 -└── apiStatsHourMan.jsp # 시간별 통계 화면 -``` - -## URL - -| 화면 | URL | -|------|-----| -| 분단위 통계 | `/kjb/statistics/apiStatsMinuteMan.view` | -| 시간별 통계 | `/kjb/statistics/apiStatsHourMan.view` | - -## 테이블 - -| 테이블명 | 설명 | -|----------|------| -| API_STATS_MINUTE | 분단위 통계 | -| API_STATS_HOUR | 시간별 통계 | - -## 복합키 구성 - -- statTime (통계 시간) -- apiName (API명) -- gwInstanceId (Gateway 인스턴스 ID) -- bizDivCode (업무구분코드) -- clientId (클라이언트 ID) -- inboundAdapter (Inbound Adapter) -- outboundAdapter (Outbound Adapter) - -## 메트릭 - -### 건수 메트릭 -- totalCnt: 총 처리 건수 -- successCnt: 성공 건수 -- timeoutCnt: Timeout 오류 건수 -- systemErrCnt: 시스템 오류 건수 -- bizErrCnt: 업무 오류 건수 -- seq900TimeoutCnt: 로그시퀀스900 Timeout 건수 -- seq900SystemErrCnt: 로그시퀀스900 시스템 오류 건수 -- seq900BizErrCnt: 로그시퀀스900 업무 오류 건수 - -### 응답시간 메트릭 (ms) -- avgRespTime: 평균 응답시간 -- minRespTime: 최소 응답시간 -- maxRespTime: 최대 응답시간 -- p50RespTime: 50 백분위 응답시간 -- p95RespTime: 95 백분위 응답시간 - -## Multi-tenancy - -API Gateway 통계는 `APIGW` 스키마에 저장됩니다. -Controller에서 `DataSourceContextHolder`를 사용하여 스키마를 전환합니다. - -```java -DataSourceContextHolder.setDataSourceType( - DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); -``` - -## 검색 조건 - -- 조회기간 (시작~종료) -- API명 (like 검색) -- Gateway 인스턴스 ID -- 업무구분코드 (like 검색) -- 클라이언트 ID (like 검색) - -## 기술 스택 - -- JPA + QueryDSL -- MapStruct (Entity ↔ UI 매핑) -- Spring MVC (ResponseEntity) -- jqGrid (목록 화면) diff --git a/src/main/java/com/eactive/ext/kjb/statistics/STATS_README.md b/src/main/java/com/eactive/ext/kjb/statistics/STATS_README.md new file mode 100644 index 0000000..69d400f --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/STATS_README.md @@ -0,0 +1,167 @@ +# API Gateway 통계 화면 + +## 개요 + +API Gateway의 분단위/시간별/일별/월별/연도별 처리 통계를 조회하는 관리 화면입니다. + +## 파일 구조 + +``` +com.eactive.ext.kjb.statistics/ +├── ApiStatsMinuteController.java # 분단위 통계 Controller +├── ApiStatsHourController.java # 시간별 통계 Controller +├── ApiStatsDayController.java # 일별 통계 Controller +├── ApiStatsMonthController.java # 월별 통계 Controller +├── ApiStatsYearController.java # 연도별 통계 Controller +├── ApiStatsAggregationController.java # 통계 집계 수동 실행 Controller +├── service/ +│ └── ApiStatsExcelExportService.java # Excel 내보내기 Service +├── entity/ +│ ├── ApiStatsMinute.java # 분단위 통계 Entity +│ ├── ApiStatsMinuteId.java # 분단위 통계 복합키 +│ ├── ApiStatsHour.java # 시간별 통계 Entity +│ ├── ApiStatsHourId.java # 시간별 통계 복합키 +│ ├── ApiStatsDay.java # 일별 통계 Entity +│ ├── ApiStatsDayId.java # 일별 통계 복합키 +│ ├── ApiStatsMonth.java # 월별 통계 Entity +│ ├── ApiStatsMonthId.java # 월별 통계 복합키 +│ ├── ApiStatsYear.java # 연도별 통계 Entity +│ └── ApiStatsYearId.java # 연도별 통계 복합키 +├── repository/ +│ ├── ApiStatsMinuteRepository.java +│ ├── ApiStatsHourRepository.java +│ ├── ApiStatsDayRepository.java +│ ├── ApiStatsMonthRepository.java +│ └── ApiStatsYearRepository.java +├── ui/ +│ ├── ApiStatsUI.java # 통계 UI DTO +│ ├── ApiStatsSearch.java # 검색조건 DTO +│ └── ApiStatsChartUI.java # 차트 UI DTO +├── mapping/ +│ └── ApiStatsUIMapper.java # Entity ↔ UI 매핑 +└── README.md + +JSP 화면: +WebContent/jsp/kjb/statistics/ +├── apiStatsMinuteMan.jsp # 분단위 통계 화면 +├── apiStatsHourMan.jsp # 시간별 통계 화면 +├── apiStatsDayMan.jsp # 일별 통계 화면 +├── apiStatsMonthMan.jsp # 월별 통계 화면 +├── apiStatsYearMan.jsp # 연도별 통계 화면 +└── apiStatsAggregationMan.jsp # 통계 집계 수동 실행 화면 +``` + +## URL + +| 화면 | URL | +|------|-----| +| 분단위 통계 | `/kjb/statistics/apiStatsMinuteMan.view` | +| 시간별 통계 | `/kjb/statistics/apiStatsHourMan.view` | +| 일별 통계 | `/kjb/statistics/apiStatsDayMan.view` | +| 월별 통계 | `/kjb/statistics/apiStatsMonthMan.view` | +| 연도별 통계 | `/kjb/statistics/apiStatsYearMan.view` | +| 통계 집계 실행 | `/kjb/statistics/apiStatsAggregationMan.view` | + +## 테이블 + +| 테이블명 | 설명 | +|----------|------| +| API_STATS_MINUTE | 분단위 통계 | +| API_STATS_HOUR | 시간별 통계 | +| API_STATS_DAY | 일별 통계 | +| API_STATS_MONTH | 월별 통계 (STAT_TIME: YYYYMM 형식) | +| API_STATS_YEAR | 연도별 통계 (STAT_TIME: YYYY 형식) | + +> 상세 테이블 스키마는 [statistics_table.md](statistics_table.md) 참조 + +## 복합키 구성 + +- statTime (통계 시간) +- apiName (API명) +- gwInstanceId (Gateway 인스턴스 ID) +- bizDivCode (업무구분코드) +- clientId (클라이언트 ID) +- inboundAdapter (Inbound Adapter) +- outboundAdapter (Outbound Adapter) + +## 메트릭 + +### 건수 메트릭 +- totalCnt: 총 처리 건수 +- successCnt: 성공 건수 +- timeoutCnt: Timeout 오류 건수 +- systemErrCnt: 시스템 오류 건수 +- bizErrCnt: 업무 오류 건수 +- seq900TimeoutCnt: 로그시퀀스900 Timeout 건수 +- seq900SystemErrCnt: 로그시퀀스900 시스템 오류 건수 +- seq900BizErrCnt: 로그시퀀스900 업무 오류 건수 + +### 응답시간 메트릭 (ms) +- avgRespTime: 평균 응답시간 +- minRespTime: 최소 응답시간 +- maxRespTime: 최대 응답시간 +- p50RespTime: 50 백분위 응답시간 +- p95RespTime: 95 백분위 응답시간 + +## Multi-tenancy + +API Gateway 통계는 `APIGW` 스키마에 저장됩니다. +Controller에서 `DataSourceContextHolder`를 사용하여 스키마를 전환합니다. + +```java +DataSourceContextHolder.setDataSourceType( + DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); +``` + +## 검색 조건 + +- 조회기간 (시작~종료) +- API명 (like 검색) +- Gateway 인스턴스 ID +- 업무구분코드 (like 검색) +- 클라이언트 ID (like 검색) + +## 주요 기능 + +- 통계 데이터 조회 (분/시간/일/월/년 단위) +- Excel 내보내기 +- 차트 데이터 제공 (ApiStatsChartUI) +- 통계 집계 수동 실행 (시간→일, 일→월, 월→년) + +## 통계 집계 수동 실행 + +통계 데이터는 스케줄러에 의해 자동으로 집계되지만, 누락된 기간이나 재집계가 필요한 경우 수동 실행할 수 있습니다. + +### 집계 실행 화면 (apiStatsAggregationMan.jsp) + +**접근 URL**: `/kjb/statistics/apiStatsAggregationMan.view` + +한 화면에서 3가지 집계 작업을 모두 수동으로 실행할 수 있습니다: + +1. **시간별 → 일별 집계** + - API 엔드포인트: `POST /onl/kjb/statistics/apiStatsDayMan.json?cmd=AGGREGATION` + - 파라미터: `targetDate` (yyyyMMdd 형식, 예: 20250120) + - 기본값: 전일 날짜 + +2. **일별 → 월별 집계** + - API 엔드포인트: `POST /onl/kjb/statistics/apiStatsMonthMan.json?cmd=AGGREGATION` + - 파라미터: `targetMonth` (yyyyMM 형식, 예: 202501) + - 기본값: 전월 + +3. **월별 → 연별 집계** + - API 엔드포인트: `POST /onl/kjb/statistics/apiStatsYearMan.json?cmd=AGGREGATION` + - 파라미터: `targetYear` (yyyy 형식, 예: 2024) + - 기본값: 전년 + +**주의사항**: +- 집계 대상 기간의 원본 데이터가 존재해야 정상 집계됩니다. +- 입력값을 비워두면 기본값(전일/전월/전년)으로 실행됩니다. +- 실행 결과는 처리 건수와 함께 화면에 표시됩니다. + +## 기술 스택 + +- JPA + QueryDSL +- MapStruct (Entity ↔ UI 매핑) +- Spring MVC (ResponseEntity) +- jqGrid (목록 화면) +- Apache POI (Excel 내보내기) 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 new file mode 100644 index 0000000..1c5872e --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/job/DailyToMonthlyAggregationJob.java @@ -0,0 +1,152 @@ +package com.eactive.ext.kjb.statistics.job; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonth; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonthService; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsDay; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsMonth; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; + +/** + * 일별 통계를 월별 통계로 집계하는 배치 작업 + * API_STATS_DAY → API_STATS_MONTH + * 실행 주기: 매월 1일 00:20 (전월 데이터 집계) + */ +@Component +public class DailyToMonthlyAggregationJob implements Job { + private static final Logger log = LoggerFactory.getLogger(DailyToMonthlyAggregationJob.class); + + private ApiStatsDayService apiStatsDayService; + private ApiStatsMonthService apiStatsMonthService; + + @Autowired + public void setApiStatsDayService(ApiStatsDayService apiStatsDayService) { + this.apiStatsDayService = apiStatsDayService; + } + + @Autowired + public void setApiStatsMonthService(ApiStatsMonthService apiStatsMonthService) { + this.apiStatsMonthService = apiStatsMonthService; + } + + @Override + @Transactional + public void execute(JobExecutionContext context) throws JobExecutionException { + try { + YearMonth targetMonth = YearMonth.now().minusMonths(1); + executeManual(targetMonth); + } catch (Exception e) { + throw new JobExecutionException(e); + } + } + + /** + * 수동 실행 메서드 (월 지정) + */ + @Transactional + public int executeManual(YearMonth targetMonth) { + long startTime = System.currentTimeMillis(); + log.info("=== 일별→월별 통계 집계 작업 시작 ==="); + + LocalDate startDate = targetMonth.atDay(1); + LocalDate endDate = targetMonth.atEndOfMonth(); + + log.info("집계 대상 기간: {} ~ {}", startDate, endDate); + + List aggregatedData = aggregateDailyToMonthly(startDate, endDate, targetMonth); + + // 1. 기존 데이터 삭제 (QueryDSL 사용) + JPAQueryFactory queryFactory = apiStatsMonthService.getJPAQueryFactory(); + QApiStatsMonth qm = QApiStatsMonth.apiStatsMonth; + String targetMonthStr = targetMonth.format(DateTimeFormatter.ofPattern("yyyyMM")); + long deletedCount = queryFactory + .delete(qm) + .where(qm.statTime.eq(targetMonthStr)) + .execute(); + log.info("기존 데이터 삭제: {} 건", deletedCount); + + // 2. 배치 INSERT + apiStatsMonthService.saveAll(aggregatedData); + int savedCount = aggregatedData.size(); + + long elapsedTime = System.currentTimeMillis() - startTime; + log.info("=== 일별→월별 통계 집계 작업 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime); + + return savedCount; + } + + /** + * 일별 데이터를 월별로 집계 + */ + private List aggregateDailyToMonthly(LocalDate startDate, LocalDate endDate, YearMonth targetMonth) { + QApiStatsDay qd = QApiStatsDay.apiStatsDay; + JPAQueryFactory queryFactory = apiStatsDayService.getJPAQueryFactory(); + + String targetMonthStr = targetMonth.format(DateTimeFormatter.ofPattern("yyyyMM")); + + List aggregatedData = queryFactory + .select(Projections.bean(ApiStatsMonth.class, + qd.apiName, + qd.gwInstanceId, + qd.bizDivCode, + qd.clientId, + qd.inboundAdapter, + qd.outboundAdapter, + qd.totalCnt.sum().as("totalCnt"), + qd.successCnt.sum().as("successCnt"), + qd.timeoutCnt.sum().as("timeoutCnt"), + qd.systemErrCnt.sum().as("systemErrCnt"), + qd.bizErrCnt.sum().as("bizErrCnt"), + qd.seq900TimeoutCnt.sum().as("seq900TimeoutCnt"), + qd.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"), + qd.seq900BizErrCnt.sum().as("seq900BizErrCnt"), + qd.avgRespTime.avg().castToNum(BigDecimal.class).as("avgRespTime"), + qd.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), + qd.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), + qd.p50RespTime.avg().castToNum(BigDecimal.class).as("p50RespTime"), + qd.p95RespTime.avg().castToNum(BigDecimal.class).as("p95RespTime"))) + .from(qd) + .where(qd.statTime.goe(startDate) + .and(qd.statTime.loe(endDate))) + .groupBy(qd.apiName, qd.gwInstanceId, qd.bizDivCode, qd.clientId, qd.inboundAdapter, qd.outboundAdapter) + .fetch(); + + for (ApiStatsMonth statsMonth : aggregatedData) { + statsMonth.setStatTime(targetMonthStr); + if (statsMonth.getAvgRespTime() != null) { + statsMonth.setAvgRespTime(statsMonth.getAvgRespTime().setScale(3, RoundingMode.HALF_UP)); + } + if (statsMonth.getP50RespTime() != null) { + statsMonth.setP50RespTime(statsMonth.getP50RespTime().setScale(3, RoundingMode.HALF_UP)); + } + if (statsMonth.getP95RespTime() != null) { + statsMonth.setP95RespTime(statsMonth.getP95RespTime().setScale(3, RoundingMode.HALF_UP)); + } + if (statsMonth.getMinRespTime() == null) { + statsMonth.setMinRespTime(BigDecimal.ZERO); + } + if (statsMonth.getMaxRespTime() == null) { + statsMonth.setMaxRespTime(BigDecimal.ZERO); + } + } + + return aggregatedData; + } +} 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 new file mode 100644 index 0000000..cfb4144 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/job/HourlyToDailyAggregationJob.java @@ -0,0 +1,148 @@ +package com.eactive.ext.kjb.statistics.job; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +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.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsDay; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsHour; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; + +/** + * 시간별 통계를 일별 통계로 집계하는 배치 작업 + * API_STATS_HOUR → API_STATS_DAY + * 실행 주기: 매일 00:10 (전일 데이터 집계) + */ +@Component +public class HourlyToDailyAggregationJob implements Job { + private static final Logger log = LoggerFactory.getLogger(HourlyToDailyAggregationJob.class); + + private ApiStatsHourService apiStatsHourService; + private ApiStatsDayService apiStatsDayService; + + @Autowired + public void setApiStatsHourService(ApiStatsHourService apiStatsHourService) { + this.apiStatsHourService = apiStatsHourService; + } + + @Autowired + public void setApiStatsDayService(ApiStatsDayService apiStatsDayService) { + this.apiStatsDayService = apiStatsDayService; + } + + @Override + @Transactional + public void execute(JobExecutionContext context) throws JobExecutionException { + try { + LocalDate targetDate = LocalDate.now().minusDays(1); + executeManual(targetDate); + } catch (Exception e) { + throw new JobExecutionException(e); + } + } + + /** + * 수동 실행 메서드 (날짜 지정) + */ + @Transactional + public int executeManual(LocalDate targetDate) { + long startTime = System.currentTimeMillis(); + log.info("=== 시간별→일별 통계 집계 작업 시작 ==="); + + LocalDateTime startDateTime = targetDate.atStartOfDay(); + LocalDateTime endDateTime = targetDate.plusDays(1).atStartOfDay().minusSeconds(1); + + log.info("집계 대상 기간: {} ~ {}", startDateTime, endDateTime); + + List aggregatedData = aggregateHourlyToDaily(startDateTime, endDateTime, targetDate); + + // 1. 기존 데이터 삭제 (QueryDSL 사용) + JPAQueryFactory queryFactory = apiStatsDayService.getJPAQueryFactory(); + QApiStatsDay qd = QApiStatsDay.apiStatsDay; + long deletedCount = queryFactory + .delete(qd) + .where(qd.statTime.eq(targetDate)) + .execute(); + log.info("기존 데이터 삭제: {} 건", deletedCount); + + // 2. 배치 INSERT + apiStatsDayService.saveAll(aggregatedData); + int savedCount = aggregatedData.size(); + + long elapsedTime = System.currentTimeMillis() - startTime; + log.info("=== 시간별→일별 통계 집계 작업 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime); + + return savedCount; + } + + /** + * 시간별 데이터를 일별로 집계 + */ + private List aggregateHourlyToDaily(LocalDateTime startDateTime, LocalDateTime endDateTime, LocalDate targetDate) { + QApiStatsHour qh = QApiStatsHour.apiStatsHour; + JPAQueryFactory queryFactory = apiStatsHourService.getJPAQueryFactory(); + + List aggregatedData = queryFactory + .select(Projections.bean(ApiStatsDay.class, + qh.apiName, + qh.gwInstanceId, + qh.bizDivCode, + qh.clientId, + qh.inboundAdapter, + qh.outboundAdapter, + qh.totalCnt.sum().as("totalCnt"), + qh.successCnt.sum().as("successCnt"), + qh.timeoutCnt.sum().as("timeoutCnt"), + qh.systemErrCnt.sum().as("systemErrCnt"), + qh.bizErrCnt.sum().as("bizErrCnt"), + qh.seq900TimeoutCnt.sum().as("seq900TimeoutCnt"), + qh.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"), + qh.seq900BizErrCnt.sum().as("seq900BizErrCnt"), + qh.avgRespTime.avg().castToNum(BigDecimal.class).as("avgRespTime"), + qh.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), + qh.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), + qh.p50RespTime.avg().castToNum(BigDecimal.class).as("p50RespTime"), + qh.p95RespTime.avg().castToNum(BigDecimal.class).as("p95RespTime"))) + .from(qh) + .where(qh.statTime.goe(startDateTime) + .and(qh.statTime.loe(endDateTime))) + .groupBy(qh.apiName, qh.gwInstanceId, qh.bizDivCode, qh.clientId, qh.inboundAdapter, qh.outboundAdapter) + .fetch(); + + for (ApiStatsDay statsDay : aggregatedData) { + statsDay.setStatTime(targetDate); + if (statsDay.getAvgRespTime() != null) { + statsDay.setAvgRespTime(statsDay.getAvgRespTime().setScale(3, RoundingMode.HALF_UP)); + } + if (statsDay.getP50RespTime() != null) { + statsDay.setP50RespTime(statsDay.getP50RespTime().setScale(3, RoundingMode.HALF_UP)); + } + if (statsDay.getP95RespTime() != null) { + statsDay.setP95RespTime(statsDay.getP95RespTime().setScale(3, RoundingMode.HALF_UP)); + } + if (statsDay.getMinRespTime() == null) { + statsDay.setMinRespTime(BigDecimal.ZERO); + } + if (statsDay.getMaxRespTime() == null) { + statsDay.setMaxRespTime(BigDecimal.ZERO); + } + } + + return aggregatedData; + } +} 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 new file mode 100644 index 0000000..c4eea0e --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/statistics/job/MonthlyToYearlyAggregationJob.java @@ -0,0 +1,150 @@ +package com.eactive.ext.kjb.statistics.job; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Year; +import java.util.List; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYear; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYearService; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonthService; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsMonth; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsYear; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; + +/** + * 월별 통계를 연별 통계로 집계하는 배치 작업 + * API_STATS_MONTH → API_STATS_YEAR + * 실행 주기: 매년 1월 1일 00:30 (전년 데이터 집계) + */ +@Component +public class MonthlyToYearlyAggregationJob implements Job { + private static final Logger log = LoggerFactory.getLogger(MonthlyToYearlyAggregationJob.class); + + private ApiStatsMonthService apiStatsMonthService; + private ApiStatsYearService apiStatsYearService; + + @Autowired + public void setApiStatsMonthService(ApiStatsMonthService apiStatsMonthService) { + this.apiStatsMonthService = apiStatsMonthService; + } + + @Autowired + public void setApiStatsYearService(ApiStatsYearService apiStatsYearService) { + this.apiStatsYearService = apiStatsYearService; + } + + @Override + @Transactional + public void execute(JobExecutionContext context) throws JobExecutionException { + try { + Year targetYear = Year.now().minusYears(1); + executeManual(targetYear); + } catch (Exception e) { + throw new JobExecutionException(e); + } + } + + /** + * 수동 실행 메서드 (연도 지정) + */ + @Transactional + public int executeManual(Year targetYear) { + long startTime = System.currentTimeMillis(); + log.info("=== 월별→연별 통계 집계 작업 시작 ==="); + + String startMonth = targetYear.toString() + "01"; + String endMonth = targetYear.toString() + "12"; + + log.info("집계 대상 기간: {} ~ {}", startMonth, endMonth); + + List aggregatedData = aggregateMonthlyToYearly(startMonth, endMonth, targetYear); + + // 1. 기존 데이터 삭제 (QueryDSL 사용) + JPAQueryFactory queryFactory = apiStatsYearService.getJPAQueryFactory(); + QApiStatsYear qy = QApiStatsYear.apiStatsYear; + String targetYearStr = targetYear.toString(); + long deletedCount = queryFactory + .delete(qy) + .where(qy.statTime.eq(targetYearStr)) + .execute(); + log.info("기존 데이터 삭제: {} 건", deletedCount); + + // 2. 배치 INSERT + apiStatsYearService.saveAll(aggregatedData); + int savedCount = aggregatedData.size(); + + long elapsedTime = System.currentTimeMillis() - startTime; + log.info("=== 월별→연별 통계 집계 작업 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime); + + return savedCount; + } + + /** + * 월별 데이터를 연별로 집계 + */ + private List aggregateMonthlyToYearly(String startMonth, String endMonth, Year targetYear) { + QApiStatsMonth qm = QApiStatsMonth.apiStatsMonth; + JPAQueryFactory queryFactory = apiStatsMonthService.getJPAQueryFactory(); + + String targetYearStr = targetYear.toString(); + + List aggregatedData = queryFactory + .select(Projections.bean(ApiStatsYear.class, + qm.apiName, + qm.gwInstanceId, + qm.bizDivCode, + qm.clientId, + qm.inboundAdapter, + qm.outboundAdapter, + qm.totalCnt.sum().as("totalCnt"), + qm.successCnt.sum().as("successCnt"), + qm.timeoutCnt.sum().as("timeoutCnt"), + qm.systemErrCnt.sum().as("systemErrCnt"), + qm.bizErrCnt.sum().as("bizErrCnt"), + qm.seq900TimeoutCnt.sum().as("seq900TimeoutCnt"), + qm.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"), + qm.seq900BizErrCnt.sum().as("seq900BizErrCnt"), + qm.avgRespTime.avg().castToNum(BigDecimal.class).as("avgRespTime"), + qm.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), + qm.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), + qm.p50RespTime.avg().castToNum(BigDecimal.class).as("p50RespTime"), + qm.p95RespTime.avg().castToNum(BigDecimal.class).as("p95RespTime"))) + .from(qm) + .where(qm.statTime.goe(startMonth) + .and(qm.statTime.loe(endMonth))) + .groupBy(qm.apiName, qm.gwInstanceId, qm.bizDivCode, qm.clientId, qm.inboundAdapter, qm.outboundAdapter) + .fetch(); + + for (ApiStatsYear statsYear : aggregatedData) { + statsYear.setStatTime(targetYearStr); + if (statsYear.getAvgRespTime() != null) { + statsYear.setAvgRespTime(statsYear.getAvgRespTime().setScale(3, RoundingMode.HALF_UP)); + } + if (statsYear.getP50RespTime() != null) { + statsYear.setP50RespTime(statsYear.getP50RespTime().setScale(3, RoundingMode.HALF_UP)); + } + if (statsYear.getP95RespTime() != null) { + statsYear.setP95RespTime(statsYear.getP95RespTime().setScale(3, RoundingMode.HALF_UP)); + } + if (statsYear.getMinRespTime() == null) { + statsYear.setMinRespTime(BigDecimal.ZERO); + } + if (statsYear.getMaxRespTime() == null) { + statsYear.setMaxRespTime(BigDecimal.ZERO); + } + } + + return aggregatedData; + } +} diff --git a/src/main/java/com/eactive/ext/kjb/statistics/mapping/ApiStatsUIMapper.java b/src/main/java/com/eactive/ext/kjb/statistics/mapping/ApiStatsUIMapper.java index 59b241b..c5fff5b 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/mapping/ApiStatsUIMapper.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/mapping/ApiStatsUIMapper.java @@ -1,5 +1,6 @@ package com.eactive.ext.kjb.statistics.mapping; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -8,8 +9,11 @@ import org.mapstruct.Mapping; import org.mapstruct.Named; import com.eactive.eai.data.mapper.BaseMapperConfig; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonth; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYear; import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; @Mapper(config = BaseMapperConfig.class) @@ -17,6 +21,7 @@ public interface ApiStatsUIMapper { DateTimeFormatter MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); DateTimeFormatter HOUR_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00"); + DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); @Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatMinuteTime") ApiStatsUI toVo(ApiStatsMinute entity); @@ -24,6 +29,15 @@ public interface ApiStatsUIMapper { @Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatHourTime") ApiStatsUI toVo(ApiStatsHour entity); + @Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatDayTime") + ApiStatsUI toVo(ApiStatsDay entity); + + @Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatMonthTime") + ApiStatsUI toVo(ApiStatsMonth entity); + + @Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatYearTime") + ApiStatsUI toVo(ApiStatsYear entity); + @Named("formatMinuteTime") default String formatMinuteTime(LocalDateTime dateTime) { if (dateTime == null) { @@ -39,4 +53,25 @@ public interface ApiStatsUIMapper { } return dateTime.format(HOUR_FORMATTER); } + + @Named("formatDayTime") + default String formatDayTime(LocalDate date) { + if (date == null) { + return null; + } + return date.format(DAY_FORMATTER); + } + + @Named("formatMonthTime") + default String formatMonthTime(String yyyymm) { + if (yyyymm == null || yyyymm.length() != 6) { + return yyyymm; + } + return yyyymm.substring(0, 4) + "-" + yyyymm.substring(4, 6); + } + + @Named("formatYearTime") + default String formatYearTime(String yyyy) { + return yyyy; + } } diff --git a/src/main/java/com/eactive/ext/kjb/statistics/statistics_table.md b/src/main/java/com/eactive/ext/kjb/statistics/statistics_table.md index 671acd5..6267b7b 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/statistics_table.md +++ b/src/main/java/com/eactive/ext/kjb/statistics/statistics_table.md @@ -167,3 +167,225 @@ COMMENT ON COLUMN API_STATS_HOUR.P95_RESP_TIME IS '95 백분위 응답시간(ms) --- +## 일별 통계 테이블 DDL + +```sql +-- API Gateway 일별 통계 테이블 +CREATE TABLE API_STATS_DAY ( + STAT_TIME DATE NOT NULL, -- 통계 시간 (일 단위) + API_NAME VARCHAR2(100) NOT NULL, -- API명 + GW_INSTANCE_ID VARCHAR2(50) NOT NULL, -- API Gateway 인스턴스 ID + BIZ_DIV_CODE VARCHAR2(50) DEFAULT 'NONE' NOT NULL, -- 업무구분코드 + CLIENT_ID VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- 클라이언트 ID + INBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Inbound Adapter + OUTBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Outbound Adapter + + -- 건수 메트릭 + TOTAL_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SUCCESS_CNT NUMBER(12) DEFAULT 0 NOT NULL, + TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + + -- 로그 시퀀스 900 구간 에러 카운트 + SEQ900_TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + + -- 응답시간 메트릭 (ms, 소수점 3자리) + AVG_RESP_TIME NUMBER(10,3) DEFAULT 0, + MIN_RESP_TIME NUMBER(10,3) DEFAULT 0, + MAX_RESP_TIME NUMBER(10,3) DEFAULT 0, + P50_RESP_TIME NUMBER(10,3) DEFAULT 0, + P95_RESP_TIME NUMBER(10,3) DEFAULT 0, + + CONSTRAINT PK_API_STATS_DAY PRIMARY KEY ( + STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID, INBOUND_ADAPTER, OUTBOUND_ADAPTER + ) +); + +-- 코멘트 +COMMENT ON TABLE API_STATS_DAY IS 'API Gateway 일별 처리 통계'; +COMMENT ON COLUMN API_STATS_DAY.STAT_TIME IS '통계시간(일단위)'; +COMMENT ON COLUMN API_STATS_DAY.API_NAME IS 'API 명칭'; +COMMENT ON COLUMN API_STATS_DAY.GW_INSTANCE_ID IS 'API Gateway 인스턴스 ID'; +COMMENT ON COLUMN API_STATS_DAY.BIZ_DIV_CODE IS '업무구분코드 (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_DAY.CLIENT_ID IS '클라이언트 ID (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_DAY.INBOUND_ADAPTER IS 'Inbound Adapter (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_DAY.OUTBOUND_ADAPTER IS 'Outbound Adapter (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_DAY.TOTAL_CNT IS '총 처리 건수'; +COMMENT ON COLUMN API_STATS_DAY.SUCCESS_CNT IS '성공 건수'; +COMMENT ON COLUMN API_STATS_DAY.TIMEOUT_CNT IS 'Timeout 오류 건수'; +COMMENT ON COLUMN API_STATS_DAY.SYSTEM_ERR_CNT IS '시스템 오류 건수'; +COMMENT ON COLUMN API_STATS_DAY.BIZ_ERR_CNT IS '업무 오류 건수'; +COMMENT ON COLUMN API_STATS_DAY.SEQ900_TIMEOUT_CNT IS '로그시퀀스900 Timeout 오류 건수'; +COMMENT ON COLUMN API_STATS_DAY.SEQ900_SYSTEM_ERR_CNT IS '로그시퀀스900 시스템 오류 건수'; +COMMENT ON COLUMN API_STATS_DAY.SEQ900_BIZ_ERR_CNT IS '로그시퀀스900 업무 오류 건수'; +COMMENT ON COLUMN API_STATS_DAY.AVG_RESP_TIME IS '평균 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_DAY.MIN_RESP_TIME IS '최소 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_DAY.MAX_RESP_TIME IS '최대 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_DAY.P50_RESP_TIME IS '50 백분위 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_DAY.P95_RESP_TIME IS '95 백분위 응답시간(ms)'; +``` + +--- + +## 월별 통계 테이블 DDL + +```sql +-- API Gateway 월별 통계 테이블 +CREATE TABLE API_STATS_MONTH ( + STAT_TIME VARCHAR2(6) NOT NULL, -- 통계 시간 (YYYYMM 형식) + API_NAME VARCHAR2(100) NOT NULL, -- API명 + GW_INSTANCE_ID VARCHAR2(50) NOT NULL, -- API Gateway 인스턴스 ID + BIZ_DIV_CODE VARCHAR2(50) DEFAULT 'NONE' NOT NULL, -- 업무구분코드 + CLIENT_ID VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- 클라이언트 ID + INBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Inbound Adapter + OUTBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Outbound Adapter + + -- 건수 메트릭 + TOTAL_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SUCCESS_CNT NUMBER(12) DEFAULT 0 NOT NULL, + TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + + -- 로그 시퀀스 900 구간 에러 카운트 + SEQ900_TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + + -- 응답시간 메트릭 (ms, 소수점 3자리) + AVG_RESP_TIME NUMBER(10,3) DEFAULT 0, + MIN_RESP_TIME NUMBER(10,3) DEFAULT 0, + MAX_RESP_TIME NUMBER(10,3) DEFAULT 0, + P50_RESP_TIME NUMBER(10,3) DEFAULT 0, + P95_RESP_TIME NUMBER(10,3) DEFAULT 0, + + CONSTRAINT PK_API_STATS_MONTH PRIMARY KEY ( + STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID, INBOUND_ADAPTER, OUTBOUND_ADAPTER + ) +); + +-- 코멘트 +COMMENT ON TABLE API_STATS_MONTH IS 'API Gateway 월별 처리 통계'; +COMMENT ON COLUMN API_STATS_MONTH.STAT_TIME IS '통계시간(월단위, YYYYMM 형식)'; +COMMENT ON COLUMN API_STATS_MONTH.API_NAME IS 'API 명칭'; +COMMENT ON COLUMN API_STATS_MONTH.GW_INSTANCE_ID IS 'API Gateway 인스턴스 ID'; +COMMENT ON COLUMN API_STATS_MONTH.BIZ_DIV_CODE IS '업무구분코드 (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_MONTH.CLIENT_ID IS '클라이언트 ID (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_MONTH.INBOUND_ADAPTER IS 'Inbound Adapter (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_MONTH.OUTBOUND_ADAPTER IS 'Outbound Adapter (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_MONTH.TOTAL_CNT IS '총 처리 건수'; +COMMENT ON COLUMN API_STATS_MONTH.SUCCESS_CNT IS '성공 건수'; +COMMENT ON COLUMN API_STATS_MONTH.TIMEOUT_CNT IS 'Timeout 오류 건수'; +COMMENT ON COLUMN API_STATS_MONTH.SYSTEM_ERR_CNT IS '시스템 오류 건수'; +COMMENT ON COLUMN API_STATS_MONTH.BIZ_ERR_CNT IS '업무 오류 건수'; +COMMENT ON COLUMN API_STATS_MONTH.SEQ900_TIMEOUT_CNT IS '로그시퀀스900 Timeout 오류 건수'; +COMMENT ON COLUMN API_STATS_MONTH.SEQ900_SYSTEM_ERR_CNT IS '로그시퀀스900 시스템 오류 건수'; +COMMENT ON COLUMN API_STATS_MONTH.SEQ900_BIZ_ERR_CNT IS '로그시퀀스900 업무 오류 건수'; +COMMENT ON COLUMN API_STATS_MONTH.AVG_RESP_TIME IS '평균 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_MONTH.MIN_RESP_TIME IS '최소 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_MONTH.MAX_RESP_TIME IS '최대 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_MONTH.P50_RESP_TIME IS '50 백분위 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_MONTH.P95_RESP_TIME IS '95 백분위 응답시간(ms)'; +``` + +--- + +## 연도별 통계 테이블 DDL + +```sql +-- API Gateway 연도별 통계 테이블 +CREATE TABLE API_STATS_YEAR ( + STAT_TIME VARCHAR2(4) NOT NULL, -- 통계 시간 (YYYY 형식) + API_NAME VARCHAR2(100) NOT NULL, -- API명 + GW_INSTANCE_ID VARCHAR2(50) NOT NULL, -- API Gateway 인스턴스 ID + BIZ_DIV_CODE VARCHAR2(50) DEFAULT 'NONE' NOT NULL, -- 업무구분코드 + CLIENT_ID VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- 클라이언트 ID + INBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Inbound Adapter + OUTBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Outbound Adapter + + -- 건수 메트릭 + TOTAL_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SUCCESS_CNT NUMBER(12) DEFAULT 0 NOT NULL, + TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + + -- 로그 시퀀스 900 구간 에러 카운트 + SEQ900_TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + SEQ900_BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL, + + -- 응답시간 메트릭 (ms, 소수점 3자리) + AVG_RESP_TIME NUMBER(10,3) DEFAULT 0, + MIN_RESP_TIME NUMBER(10,3) DEFAULT 0, + MAX_RESP_TIME NUMBER(10,3) DEFAULT 0, + P50_RESP_TIME NUMBER(10,3) DEFAULT 0, + P95_RESP_TIME NUMBER(10,3) DEFAULT 0, + + CONSTRAINT PK_API_STATS_YEAR PRIMARY KEY ( + STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID, INBOUND_ADAPTER, OUTBOUND_ADAPTER + ) +); + +-- 코멘트 +COMMENT ON TABLE API_STATS_YEAR IS 'API Gateway 연도별 처리 통계'; +COMMENT ON COLUMN API_STATS_YEAR.STAT_TIME IS '통계시간(연도단위, YYYY 형식)'; +COMMENT ON COLUMN API_STATS_YEAR.API_NAME IS 'API 명칭'; +COMMENT ON COLUMN API_STATS_YEAR.GW_INSTANCE_ID IS 'API Gateway 인스턴스 ID'; +COMMENT ON COLUMN API_STATS_YEAR.BIZ_DIV_CODE IS '업무구분코드 (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_YEAR.CLIENT_ID IS '클라이언트 ID (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_YEAR.INBOUND_ADAPTER IS 'Inbound Adapter (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_YEAR.OUTBOUND_ADAPTER IS 'Outbound Adapter (없으면 NONE)'; +COMMENT ON COLUMN API_STATS_YEAR.TOTAL_CNT IS '총 처리 건수'; +COMMENT ON COLUMN API_STATS_YEAR.SUCCESS_CNT IS '성공 건수'; +COMMENT ON COLUMN API_STATS_YEAR.TIMEOUT_CNT IS 'Timeout 오류 건수'; +COMMENT ON COLUMN API_STATS_YEAR.SYSTEM_ERR_CNT IS '시스템 오류 건수'; +COMMENT ON COLUMN API_STATS_YEAR.BIZ_ERR_CNT IS '업무 오류 건수'; +COMMENT ON COLUMN API_STATS_YEAR.SEQ900_TIMEOUT_CNT IS '로그시퀀스900 Timeout 오류 건수'; +COMMENT ON COLUMN API_STATS_YEAR.SEQ900_SYSTEM_ERR_CNT IS '로그시퀀스900 시스템 오류 건수'; +COMMENT ON COLUMN API_STATS_YEAR.SEQ900_BIZ_ERR_CNT IS '로그시퀀스900 업무 오류 건수'; +COMMENT ON COLUMN API_STATS_YEAR.AVG_RESP_TIME IS '평균 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_YEAR.MIN_RESP_TIME IS '최소 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_YEAR.MAX_RESP_TIME IS '최대 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_YEAR.P50_RESP_TIME IS '50 백분위 응답시간(ms)'; +COMMENT ON COLUMN API_STATS_YEAR.P95_RESP_TIME IS '95 백분위 응답시간(ms)'; +``` + +--- + +## 샘플 데이터 (테스트용) + +```sql +-- 월별 통계 샘플 데이터 +INSERT INTO API_STATS_MONTH ( + 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 +) VALUES ( + '202512', 'TEST_API', 'GW01', 'MOBILE', 'CLIENT01', + 'HTTP', 'REST', 30000, 29400, 300, 150, 150, 60, 30, 60, + 120.500, 8.000, 600.000, 95.000, 280.000 +); + +-- 연도별 통계 샘플 데이터 +INSERT INTO API_STATS_YEAR ( + 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 +) VALUES ( + '2025', 'TEST_API', 'GW01', 'MOBILE', 'CLIENT01', + 'HTTP', 'REST', 365000, 357700, 3650, 1825, 1825, 730, 365, 730, + 118.750, 5.000, 700.000, 90.000, 270.000 +); + +COMMIT; +``` + +--- diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsChartUI.java b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsChartUI.java index 8a7eb86..3f788be 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsChartUI.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsChartUI.java @@ -1,6 +1,7 @@ package com.eactive.ext.kjb.statistics.ui; import java.math.BigDecimal; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -30,12 +31,43 @@ public class ApiStatsChartUI { private BigDecimal p50RespTime; private BigDecimal p95RespTime; - // QueryDSL Projections용 생성자 + // QueryDSL Projections용 생성자 - Hour/Minute (LocalDateTime) public ApiStatsChartUI(LocalDateTime statTime, Long totalCnt, Long successCnt, Long timeoutCnt, Long systemErrCnt, Long bizErrCnt, Long seq900TimeoutCnt, Long seq900SystemErrCnt, Long seq900BizErrCnt, Double avgRespTime, Double p50RespTime, Double p95RespTime) { this.statTime = statTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")); + setMetrics(totalCnt, successCnt, timeoutCnt, systemErrCnt, bizErrCnt, + seq900TimeoutCnt, seq900SystemErrCnt, seq900BizErrCnt, + avgRespTime, p50RespTime, p95RespTime); + } + + // QueryDSL Projections용 생성자 - Day (LocalDate) + public ApiStatsChartUI(LocalDate statTime, Long totalCnt, Long successCnt, + Long timeoutCnt, Long systemErrCnt, Long bizErrCnt, + Long seq900TimeoutCnt, Long seq900SystemErrCnt, Long seq900BizErrCnt, + Double avgRespTime, Double p50RespTime, Double p95RespTime) { + this.statTime = statTime.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + setMetrics(totalCnt, successCnt, timeoutCnt, systemErrCnt, bizErrCnt, + seq900TimeoutCnt, seq900SystemErrCnt, seq900BizErrCnt, + avgRespTime, p50RespTime, p95RespTime); + } + + // QueryDSL Projections용 생성자 - Month/Year (String) + public ApiStatsChartUI(String statTime, Long totalCnt, Long successCnt, + Long timeoutCnt, Long systemErrCnt, Long bizErrCnt, + Long seq900TimeoutCnt, Long seq900SystemErrCnt, Long seq900BizErrCnt, + Double avgRespTime, Double p50RespTime, Double p95RespTime) { + this.statTime = statTime; + setMetrics(totalCnt, successCnt, timeoutCnt, systemErrCnt, bizErrCnt, + seq900TimeoutCnt, seq900SystemErrCnt, seq900BizErrCnt, + avgRespTime, p50RespTime, p95RespTime); + } + + private void setMetrics(Long totalCnt, Long successCnt, Long timeoutCnt, + Long systemErrCnt, Long bizErrCnt, Long seq900TimeoutCnt, + Long seq900SystemErrCnt, Long seq900BizErrCnt, + Double avgRespTime, Double p50RespTime, Double p95RespTime) { this.totalCnt = totalCnt != null ? totalCnt : 0L; this.successCnt = successCnt != null ? successCnt : 0L; this.timeoutCnt = timeoutCnt != null ? timeoutCnt : 0L; 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 68c75cd..96c0e58 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 @@ -1,5 +1,6 @@ package com.eactive.ext.kjb.statistics.ui; +import java.time.LocalDate; import java.time.LocalDateTime; import lombok.Data; @@ -22,4 +23,19 @@ public class ApiStatsSearch { private LocalDateTime parsedStartDateTime; /** 파싱된 종료시간 */ private LocalDateTime parsedEndDateTime; + + /** 파싱된 시작날짜 (Day 통계용) */ + private LocalDate parsedStartDate; + /** 파싱된 종료날짜 (Day 통계용) */ + private LocalDate parsedEndDate; + + /** 파싱된 시작월 (Month 통계용, YYYYMM) */ + private String parsedStartMonth; + /** 파싱된 종료월 (Month 통계용, YYYYMM) */ + private String parsedEndMonth; + + /** 파싱된 시작년 (Year 통계용, YYYY) */ + private String parsedStartYear; + /** 파싱된 종료년 (Year 통계용, YYYY) */ + private String parsedEndYear; }