From 0b341b85f1d1e12c1d955c3f78ea9aed4ebd036b Mon Sep 17 00:00:00 2001 From: eastargh Date: Thu, 14 May 2026 11:17:55 +0900 Subject: [PATCH 01/45] =?UTF-8?q?=ED=8F=89=EA=B7=A0=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=86=8D=EB=8F=84=20=EA=B3=84=EC=82=B0=EC=8B=9C=20=EB=B6=84?= =?UTF-8?q?=EB=AA=A8=EB=A5=BC=20=EC=84=B1=EA=B3=B5=EA=B1=B4=EC=88=98?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B4=9D=EA=B1=B4=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onl/kjb/statistics/ApiStatsDayService.java | 8 ++++---- .../onl/kjb/statistics/ApiStatsHourService.java | 13 ++++++------- .../onl/kjb/statistics/ApiStatsMonthService.java | 14 +++++++------- .../onl/kjb/statistics/ApiStatsYearService.java | 14 +++++++------- .../job/DailyToMonthlyAggregationJob.java | 6 +++--- .../job/HourlyToDailyAggregationJob.java | 6 +++--- .../job/MonthlyToYearlyAggregationJob.java | 6 +++--- 7 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayService.java index ad7fc2e..efe1566 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsDayService.java @@ -120,11 +120,11 @@ public class ApiStatsDayService q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), - // 가중 평균: SUM(avg * successCnt) / SUM(successCnt) + // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt) Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java index 91524a7..6d01415 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsHourService.java @@ -211,11 +211,10 @@ public class ApiStatsHourService q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), - // 가중 평균: SUM(avg * successCnt) / SUM(successCnt) Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) @@ -293,9 +292,9 @@ public class ApiStatsHourService q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthService.java index 4545eca..6bb2e3a 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsMonthService.java @@ -120,11 +120,11 @@ public class ApiStatsMonthService q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), - // 가중 평균: SUM(avg * successCnt) / SUM(successCnt) + // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt) Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) @@ -172,9 +172,9 @@ public class ApiStatsMonthService q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearService.java index 65842b0..8d2f080 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/kjb/statistics/ApiStatsYearService.java @@ -117,11 +117,11 @@ public class ApiStatsYearService q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), - // 가중 평균: SUM(avg * successCnt) / SUM(successCnt) + // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt) Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) @@ -169,9 +169,9 @@ public class ApiStatsYearService q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), Expressions.cases() - .when(q.successCnt.sum().gt(0)) - .then(q.avgRespTime.multiply(q.successCnt).sum() - .divide(q.successCnt.sum()).castToNum(BigDecimal.class)) + .when(q.totalCnt.sum().gt(0)) + .then(q.avgRespTime.multiply(q.totalCnt).sum() + .divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"))) .from(q) diff --git a/src/main/java/com/eactive/ext/kjb/statistics/job/DailyToMonthlyAggregationJob.java b/src/main/java/com/eactive/ext/kjb/statistics/job/DailyToMonthlyAggregationJob.java index cb25000..c6e56ba 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/job/DailyToMonthlyAggregationJob.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/job/DailyToMonthlyAggregationJob.java @@ -138,9 +138,9 @@ public class DailyToMonthlyAggregationJob implements Job { qd.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"), qd.seq900BizErrCnt.sum().as("seq900BizErrCnt"), Expressions.cases() - .when(qd.successCnt.sum().gt(0)) - .then(qd.avgRespTime.multiply(qd.successCnt).sum() - .divide(qd.successCnt.sum()).castToNum(BigDecimal.class)) + .when(qd.totalCnt.sum().gt(0)) + .then(qd.avgRespTime.multiply(qd.totalCnt).sum() + .divide(qd.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"), qd.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), diff --git a/src/main/java/com/eactive/ext/kjb/statistics/job/HourlyToDailyAggregationJob.java b/src/main/java/com/eactive/ext/kjb/statistics/job/HourlyToDailyAggregationJob.java index a71b783..97729b7 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/job/HourlyToDailyAggregationJob.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/job/HourlyToDailyAggregationJob.java @@ -133,9 +133,9 @@ public class HourlyToDailyAggregationJob implements Job { qh.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"), qh.seq900BizErrCnt.sum().as("seq900BizErrCnt"), Expressions.cases() - .when(qh.successCnt.sum().gt(0)) - .then(qh.avgRespTime.multiply(qh.successCnt).sum() - .divide(qh.successCnt.sum()).castToNum(BigDecimal.class)) + .when(qh.totalCnt.sum().gt(0)) + .then(qh.avgRespTime.multiply(qh.totalCnt).sum() + .divide(qh.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"), qh.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), diff --git a/src/main/java/com/eactive/ext/kjb/statistics/job/MonthlyToYearlyAggregationJob.java b/src/main/java/com/eactive/ext/kjb/statistics/job/MonthlyToYearlyAggregationJob.java index c3fe895..a7ebf6c 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/job/MonthlyToYearlyAggregationJob.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/job/MonthlyToYearlyAggregationJob.java @@ -136,9 +136,9 @@ public class MonthlyToYearlyAggregationJob implements Job { qm.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"), qm.seq900BizErrCnt.sum().as("seq900BizErrCnt"), Expressions.cases() - .when(qm.successCnt.sum().gt(0)) - .then(qm.avgRespTime.multiply(qm.successCnt).sum() - .divide(qm.successCnt.sum()).castToNum(BigDecimal.class)) + .when(qm.totalCnt.sum().gt(0)) + .then(qm.avgRespTime.multiply(qm.totalCnt).sum() + .divide(qm.totalCnt.sum()).castToNum(BigDecimal.class)) .otherwise((BigDecimal) null) .as("avgRespTime"), qm.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), From 335c7ec53aa0fe198cb972fb13026779ccf33108 Mon Sep 17 00:00:00 2001 From: eastargh Date: Thu, 14 May 2026 11:19:55 +0900 Subject: [PATCH 02/45] =?UTF-8?q?=EA=B1=B0=EB=9E=98=EB=A1=9C=EA=B7=B8=20>?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=EB=B3=84=20=EC=A7=91=EA=B3=84=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=B0=B0=EC=B9=98=EC=9E=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../djb/job/ApiStatsHourlyAggregationJob.java | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java b/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java new file mode 100644 index 0000000..b29c7f3 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java @@ -0,0 +1,147 @@ +package com.eactive.eai.rms.ext.djb.job; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; +import com.eactive.eai.rms.common.datasource.DataSourceType; +import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; +import com.eactive.eai.rms.common.util.CommonUtil; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsHour; + +/** + * API 로그 테이블을 시간별 통계로 집계하는 배치 작업 + * TSEAILGXX → API_STATS_HOUR + * 실행 주기: 매시 00:10 (당일 데이터 집계) + */ +@Component +public class ApiStatsHourlyAggregationJob implements Job { + private static final Logger log = LoggerFactory.getLogger(ApiStatsHourlyAggregationJob.class); + + @PersistenceContext + private EntityManager entityManager; + + private ApiStatsHourService apiStatsHourService; + + @Autowired + public void setApiStatsHourService(ApiStatsHourService apiStatsHourService) { + this.apiStatsHourService = apiStatsHourService; + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + ApplicationContext appContext = null; + final ApiStatsHourlyAggregationJob selfJob; + try { + appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext"); + selfJob = appContext.getBean(ApiStatsHourlyAggregationJob.class); + } catch (SchedulerException e) { + log.error("applicationContext get module error", e); + return; + } + + DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW); + DataSourceContextHolder.setDataSourceType(dataType); + try { + LocalDate targetDate = LocalDate.now(); + if (LocalTime.now().isBefore(LocalTime.of(1, 0))) { + targetDate = targetDate.minusDays(1); + } + selfJob.executeManual(targetDate); + } catch (Exception e) { + throw new JobExecutionException(e); + } finally { + DataSourceContextHolder.clearDataSourceType(); + } + } + + /** + * 수동 실행 메서드 (날짜 지정) + */ + @Transactional + public int executeManual(LocalDate targetDate) { + long startTime = System.currentTimeMillis(); + log.info("=== 시간별 통계 집계 작업 시작 === 대상: {}", targetDate); + + String searchDate = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String schemaEMS = System.getProperty("eai.tableowner", "EMSADM").toUpperCase(); + String logTableName = CommonUtil.getLogTable(searchDate, true); + + // 1. 기존 데이터 삭제 + QApiStatsHour q = QApiStatsHour.apiStatsHour; + long deletedCount = apiStatsHourService.getJPAQueryFactory() + .delete(q) + .where(q.statTime.goe(targetDate.atStartOfDay()) + .and(q.statTime.lt(targetDate.plusDays(1).atStartOfDay()))) + .execute(); + log.info("기존 데이터 삭제: {} 건", deletedCount); + + // 2. 집계 및 데이터 입력 + /** + * EAISVCSERNO로 그룹핑하여 1행으로 집계를 한 다음 + * 총건수 : EAISVCSERNO로 그룹핑한 총 건수 + * 성공건수 : seq = 400 and EAIERRCD = null 인 건수 + * Timeout : EAIERRCD in (공통코드 CODEGROUP = 'API_ERROR_CODE' and PARENTCODE = 'T') 인 건수 + * 업무오류 : EAIERRCD in (공통코드 CODEGROUP = 'API_ERROR_CODE' and PARENTCODE = 'B') 인 건수 + * 시스템오류 : (seq400 = null or EAIERRCD is not null) and 공통코드에 정의되지 않은 errorcode 인 건수 + */ + String sql = + "INSERT INTO API_STATS_HOUR" + + " (STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID" + + " , INBOUND_ADAPTER, OUTBOUND_ADAPTER" + + ", TOTAL_CNT, SUCCESS_CNT, TIMEOUT_CNT, SYSTEM_ERR_CNT, BIZ_ERR_CNT" + + ", SEQ900_TIMEOUT_CNT, SEQ900_SYSTEM_ERR_CNT, SEQ900_BIZ_ERR_CNT" + + ", AVG_RESP_TIME, MIN_RESP_TIME, MAX_RESP_TIME, P50_RESP_TIME, P95_RESP_TIME)" + + " SELECT TO_TIMESTAMP(SUBSTR(DT,1,10) || '0000', 'YYYYMMDDHH24MISS')" + + " , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" + + " , INBOUND_ADAPTER, OUTBOUND_ADAPTER" + + " , COUNT(EAISVCSERNO)" + + " , SUM(CASE WHEN A.ERROR_CODE IS NULL AND E400 IS NOT NULL THEN 1 ELSE 0 END)" + + " , SUM(CASE WHEN B.PARENTCODE = 'T' THEN 1 ELSE 0 END)" + + " , SUM(CASE WHEN (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.PARENTCODE IS NULL THEN 1 ELSE 0 END)" + + " , SUM(CASE WHEN B.PARENTCODE = 'B' THEN 1 ELSE 0 END)" + + " , 0, 0, 0" + + " , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" + + " FROM (" + + " SELECT EAISVCSERNO" + + " , MAX(EAISVCNAME) AS API_NAME, MAX(EAISEVRINSTNCNAME) AS GW_INSTANCE_ID" + + " , MAX(EAIBZWKDSTCD) AS BIZ_DIV_CODE, MAX(CLIENTID) AS CLIENT_ID" + + " , MAX(CLIENTNAME) AS CLIENT_NAME, MAX(ORGID) AS ORG_ID, MAX(ORGNAME) AS ORG_NAME" + + " , MAX(GSTATSYSADPTRBZWKGROUPNAME) AS INBOUND_ADAPTER" + + " , MAX(PSVSYSADPTRBZWKGROUPNAME) AS OUTBOUND_ADAPTER" + + " , MIN(MSGDPSTYMS) AS DT" + + " , MAX(MSGPRCSSYMS) - MIN(MSGDPSTYMS) AS RESP_TIME" + + " , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" + + " , MAX(EAIERRCD) AS ERROR_CODE" + + " FROM " + logTableName + + " WHERE MSGDPSTYMS LIKE :searchDate || '%'" + + " GROUP BY EAISVCSERNO" + + " ) A LEFT OUTER JOIN " + schemaEMS + ".TSEAIRM28 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'API_ERROR_CODE'" + + " GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" + + " , INBOUND_ADAPTER, OUTBOUND_ADAPTER"; + + int savedCount = entityManager.createNativeQuery(sql) + .setParameter("searchDate", searchDate) + .executeUpdate(); + + long elapsedTime = System.currentTimeMillis() - startTime; + log.info("=== 집계 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime); + return savedCount; + } +} From 45c6edbbebf6bd924d1f99fdf7915472ab95a6f2 Mon Sep 17 00:00:00 2001 From: eastargh Date: Thu, 14 May 2026 11:20:33 +0900 Subject: [PATCH 03/45] =?UTF-8?q?=EA=B1=B0=EB=9E=98=EB=A1=9C=EA=B7=B8>?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EB=B3=84=20=EC=88=98=EB=8F=99=EC=A7=91?= =?UTF-8?q?=EA=B3=84=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kjb/statistics/apiStatsAggregationMan.jsp | 77 ++++++++++++++++++- .../ApiStatsAggregationController.java | 51 ++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp b/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp index 8322d71..cebd75b 100644 --- a/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp +++ b/WebContent/jsp/onl/kjb/statistics/apiStatsAggregationMan.jsp @@ -82,6 +82,54 @@ + + + +
+
+ +
+
+
+ + +
+
API 사용현황
+ + + + + + + + + + + + + + + +
조회기간 + + ~ + + (최대 31일) +
조회구분 + + + + 제휴사명 + + API명 + +
+ +
+
+
+
+ + +
+
+ + diff --git a/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsExcelExportService.java b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsExcelExportService.java new file mode 100644 index 0000000..b79aae2 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsExcelExportService.java @@ -0,0 +1,256 @@ +package com.eactive.eai.rms.data.ext.djb.statistics; + +import java.io.IOException; +import java.math.BigDecimal; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; + +import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; + +import lombok.extern.slf4j.Slf4j; + +/** + * API 통계 Excel Export 서비스 + * Hour/Minute 통계 데이터를 Excel 파일로 생성 + */ +@Slf4j +@Service +public class ApiUseStatsExcelExportService { + + private static final String[] COLUMN_HEADERS = { + "구분", "총건수", "성공", "성공율(%)", "실패율(%)", "Timeout", "시스템오류", + "평균응답(ms)", "최소응답(ms)", "최대응답(ms)" + }; + + /** + * API 통계 데이터를 Excel 파일로 생성하여 HTTP 응답으로 전송 + * + * @param dataList API 통계 데이터 리스트 + * @param fileName 다운로드 파일명 + * @param response HTTP 응답 객체 + * @throws IOException 파일 생성 실패 시 + */ + public void exportApiStats(List dataList, String fileName, HttpServletResponse response) + throws IOException { + + log.info("Starting Excel export - rows: {}, filename: {}", dataList.size(), fileName); + + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("API통계"); + log.debug("Excel sheet created"); + + // 스타일 생성 + CellStyle headerStyle = createHeaderStyle(workbook); + CellStyle stringStyle = createStringStyle(workbook); + CellStyle numberStyle = createNumberStyle(workbook); + CellStyle decimalStyle = createDecimalStyle(workbook); + log.debug("Excel styles created"); + + // 헤더 행 생성 + createHeaderRow(sheet, headerStyle); + log.debug("Header row created"); + + // 데이터 행 생성 + int rowNum = 1; + for (ApiStatsUI data : dataList) { + createDataRow(sheet, rowNum++, data, stringStyle, numberStyle, decimalStyle); + } + log.info("Data rows created - count: {}", dataList.size()); + + // 컬럼 너비 자동 조정 + autoSizeColumns(sheet); + log.debug("Column widths adjusted"); + + // HTTP 응답 설정 + setHttpResponse(response, fileName, workbook); + log.info("Excel file sent to response"); + + } catch (Exception e) { + log.error("Error creating Excel file", e); + throw new IOException("Excel file creation failed: " + e.getMessage(), e); + } + } + + /** + * 헤더 스타일 생성 (회색 배경, 볼드, 테두리) + */ + private CellStyle createHeaderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + + // 배경색 + style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + // 테두리 + style.setBorderTop(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + + // 정렬 + style.setAlignment(HorizontalAlignment.CENTER); + + // 폰트 (볼드) + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + + return style; + } + + /** + * 문자열 데이터 스타일 생성 (기본 테두리) + */ + private CellStyle createStringStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setBorderTop(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + return style; + } + + /** + * 숫자 데이터 스타일 생성 (우측정렬 + 쉼표 구분) + */ + private CellStyle createNumberStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setBorderTop(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setAlignment(HorizontalAlignment.RIGHT); + style.setDataFormat(workbook.createDataFormat().getFormat("#,##0")); + return style; + } + + /** + * 소수점 데이터 스타일 생성 (우측정렬 + 소수점 2자리) + */ + private CellStyle createDecimalStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setBorderTop(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setAlignment(HorizontalAlignment.RIGHT); + style.setDataFormat(workbook.createDataFormat().getFormat("0.00")); + return style; + } + + /** + * 헤더 행 생성 + */ + private void createHeaderRow(Sheet sheet, CellStyle headerStyle) { + Row headerRow = sheet.createRow(0); + + for (int i = 0; i < COLUMN_HEADERS.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(COLUMN_HEADERS[i]); + cell.setCellStyle(headerStyle); + } + } + + /** + * 데이터 행 생성 + */ + private void createDataRow(Sheet sheet, int rowNum, ApiStatsUI data, + CellStyle stringStyle, CellStyle numberStyle, CellStyle decimalStyle) { + + Row row = sheet.createRow(rowNum); + int colNum = 0; + + createStringCell(row, colNum++, data.getOrgName(), stringStyle); + createLongCell(row, colNum++, data.getTotalCnt(), numberStyle); + createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle); + createDecimalCell(row, colNum++, data.getSuccessRate(), decimalStyle); + createDecimalCell(row, colNum++, data.getFailRate(), decimalStyle); + createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle); + createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle); + createDecimalCell(row, colNum++, data.getAvgRespTime(), numberStyle); + createDecimalCell(row, colNum++, data.getMinRespTime(), numberStyle); + createDecimalCell(row, colNum++, data.getMaxRespTime(), numberStyle); + } + + /** + * 문자열 셀 생성 + */ + private void createStringCell(Row row, int colNum, String value, CellStyle style) { + Cell cell = row.createCell(colNum); + cell.setCellValue(value != null ? value : ""); + cell.setCellStyle(style); + } + + /** + * Long 타입 숫자 셀 생성 + */ + private void createLongCell(Row row, int colNum, Long value, CellStyle style) { + Cell cell = row.createCell(colNum); + if (value != null) { + cell.setCellValue(value.doubleValue()); + } else { + cell.setCellValue(0); + } + cell.setCellStyle(style); + } + + /** + * BigDecimal 타입 소수점 셀 생성 + */ + private void createDecimalCell(Row row, int colNum, BigDecimal value, CellStyle style) { + Cell cell = row.createCell(colNum); + if (value != null) { + cell.setCellValue(value.doubleValue()); + } else { + cell.setCellValue(0.0); + } + cell.setCellStyle(style); + } + + /** + * 컬럼 너비 자동 조정 + */ + private void autoSizeColumns(Sheet sheet) { + for (int i = 0; i < COLUMN_HEADERS.length; i++) { + sheet.autoSizeColumn(i); + // 한글 문자 고려하여 약간 여유 공간 추가 + sheet.setColumnWidth(i, sheet.getColumnWidth(i) + 512); + } + } + + /** + * HTTP 응답 설정 및 파일 전송 + */ + private void setHttpResponse(HttpServletResponse response, String fileName, Workbook workbook) + throws IOException { + + // Content-Type 설정 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + + // Content-Disposition 설정 (파일명 UTF-8 인코딩) + String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()) + .replaceAll("\\+", "%20"); + response.setHeader("Content-Disposition", + "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName); + + // Workbook을 응답 스트림으로 전송 + workbook.write(response.getOutputStream()); + response.getOutputStream().flush(); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsRepository.java b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsRepository.java new file mode 100644 index 0000000..055127d --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsRepository.java @@ -0,0 +1,12 @@ +package com.eactive.eai.rms.data.ext.djb.statistics; + +import com.eactive.eai.data.jpa.BaseRepository; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId; + +/** + * API 사용현황 Repository + */ +interface ApiUseStatsRepository extends BaseRepository { + +} diff --git a/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsService.java b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsService.java new file mode 100644 index 0000000..e6923f6 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/ext/djb/statistics/ApiUseStatsService.java @@ -0,0 +1,211 @@ +package com.eactive.eai.rms.data.ext.djb.statistics; + +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.eactive.eai.data.jpa.AbstractDataService; +import com.eactive.eai.rms.common.util.StringUtils; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay; +import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId; +import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; +import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; + +@Service +@Transactional +public class ApiUseStatsService + extends AbstractDataService { + + private static final int MAX_DAYS = 31; + + @PersistenceContext + private EntityManager entityManager; + + @SuppressWarnings("unchecked") + public Page selectList(ApiStatsSearch search, Pageable pageable) { + Query dataQuery = getDataQuery(search); + dataQuery.setFirstResult((int) pageable.getOffset()); + dataQuery.setMaxResults(pageable.getPageSize()); + + List rows = dataQuery.getResultList(); + List results = rows.stream() + .map(this::toVO) + .collect(Collectors.toList()); + + long records = rows.size() > 0 ? StringUtils.toLong(rows.get(0)[0]) : 0; + + return new PageImpl<>(results, pageable, records); + } + + + @SuppressWarnings("unchecked") + public List selectList(ApiStatsSearch search) { + Query dataQuery = getDataQuery(search); + List rows = dataQuery.getResultList(); + return rows.stream() + .map(this::toVO) + .collect(Collectors.toList()); + } + + + private Query getDataQuery(ApiStatsSearch search) { + + String schemaEms = System.getProperty("eai.tableowner", "EMSADM").toUpperCase(); + + StringBuilder where = buildNativeWhere(search); + + String dataSql = ""; + if ("ORG".equals(search.getSearchType())) { + dataSql = "SELECT COUNT(*) OVER () AS RECORDS " + + ", B.ORGNAME" + + ", SUM(A.TOTAL_CNT)" + + ", SUM(A.SUCCESS_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", SUM(A.TIMEOUT_CNT)" + + ", SUM(A.SYSTEM_ERR_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END" + + ", MIN(A.MIN_RESP_TIME)" + + ", MAX(A.MAX_RESP_TIME)" + + " FROM API_STATS_DAY A" + + " LEFT OUTER JOIN " + schemaEms + ".PTL_CREDENTIAL B ON A.CLIENT_ID = B.CLIENTID" + + " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME " + + where + + " GROUP BY B.ORGNAME" + + " ORDER BY ORGNAME"; + } else if ("API".equals(search.getSearchType())) { + dataSql = "SELECT COUNT(*) OVER () AS RECORDS " + + ", C.EAISVCDESC" + + ", SUM(A.TOTAL_CNT)" + + ", SUM(A.SUCCESS_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", SUM(A.TIMEOUT_CNT)" + + ", SUM(A.SYSTEM_ERR_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END" + + ", MIN(A.MIN_RESP_TIME)" + + ", MAX(A.MAX_RESP_TIME)" + + " FROM API_STATS_DAY A " + + " LEFT OUTER JOIN " + schemaEms + ".PTL_CREDENTIAL B ON A.CLIENT_ID = B.CLIENTID" + + " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME " + + where + + " GROUP BY C.EAISVCDESC" + + " ORDER BY EAISVCDESC"; + } else if ("DATE".equals(search.getSearchType())) { + dataSql = "SELECT COUNT(*) OVER () AS RECORDS " + + ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME " + + ", SUM(A.TOTAL_CNT)" + + ", SUM(A.SUCCESS_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END" + + ", SUM(A.TIMEOUT_CNT)" + + ", SUM(A.SYSTEM_ERR_CNT)" + + ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END" + + ", MIN(A.MIN_RESP_TIME)" + + ", MAX(A.MAX_RESP_TIME)" + + " FROM API_STATS_DAY A " + + " LEFT OUTER JOIN " + schemaEms + ".PTL_CREDENTIAL B ON A.CLIENT_ID = B.CLIENTID" + + " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME " + + where + + " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')" + + " ORDER BY STAT_TIME"; + } + + Query dataQuery = entityManager.createNativeQuery(dataSql); + if (search.getParsedStartDate() != null) { + dataQuery.setParameter("searchStartDate", search.getParsedStartDate()); + dataQuery.setParameter("searchEndDate", search.getParsedEndDate()); + } + if (StringUtils.isNotBlank(search.getSearchOrgName())) { + dataQuery.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%"); + } + if (StringUtils.isNotBlank(search.getSearchApiName())) { + dataQuery.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%"); + } + return dataQuery; + } + + + /** + * Native Query 결과를 ApiStatsUI로 변환 + */ + private ApiStatsUI toVO(Object[] row) { + ApiStatsUI vo = new ApiStatsUI(); + vo.setOrgName(StringUtils.toString(row[1])); + vo.setTotalCnt(StringUtils.toLong(row[2])); + vo.setSuccessCnt(StringUtils.toLong(row[3])); + vo.setSuccessRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP)); + vo.setFailRate(StringUtils.toDecimal(row[5]).setScale(2, RoundingMode.HALF_UP)); + vo.setTimeoutCnt(StringUtils.toLong(row[6])); + vo.setSystemErrCnt(StringUtils.toLong(row[7])); + vo.setAvgRespTime(StringUtils.toDecimal(row[8])); + vo.setMinRespTime(StringUtils.toDecimal(row[9])); + vo.setMaxRespTime(StringUtils.toDecimal(row[10])); + return vo; + } + + + /** + * native SQL용 동적 WHERE 절 생성 + */ + private StringBuilder buildNativeWhere(ApiStatsSearch search) { + StringBuilder where = new StringBuilder(" WHERE 1=1"); + + calculateDateRange(search); + + if (search.getParsedStartDate() != null) { + where.append(" AND A.STAT_TIME >= :searchStartDate "); + where.append(" AND A.STAT_TIME <= :searchEndDate "); + } + + if (StringUtils.isNotBlank(search.getSearchOrgName())) { + where.append(" AND UPPER(B.ORGNAME) LIKE :searchOrgName "); + } + + if (StringUtils.isNotBlank(search.getSearchApiName())) { + where.append(" AND UPPER(C.EAISVCDESC) LIKE :searchApiName "); + } + + return where; + } + + + /** + * 조회 기간 계산 (최대 31일 제한) + */ + private void calculateDateRange(ApiStatsSearch search) { + if (StringUtils.isBlank(search.getSearchStartDateTime())) { + return; + } + + LocalDate startDate = LocalDate.parse(search.getSearchStartDateTime(), + DateTimeFormatter.ofPattern("yyyyMMdd")); + LocalDate endDate; + + if (StringUtils.isNotBlank(search.getSearchEndDateTime())) { + endDate = LocalDate.parse(search.getSearchEndDateTime(), + DateTimeFormatter.ofPattern("yyyyMMdd")); + if (java.time.Period.between(startDate, endDate).getDays() > MAX_DAYS) { + endDate = startDate.plusDays(MAX_DAYS); + } + } else { + endDate = startDate.plusDays(MAX_DAYS); + } + + search.setParsedStartDate(startDate); + search.setParsedEndDate(endDate); + } + +} diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java b/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java index d637000..606f50c 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/job/ApiStatsHourlyAggregationJob.java @@ -94,8 +94,8 @@ public class ApiStatsHourlyAggregationJob implements Job { // 2. 집계 및 데이터 입력 /** - * EAISVCSERNO로 그룹핑하여 1행으로 집계를 한 다음 - * 총건수 : EAISVCSERNO로 그룹핑한 총 건수 + * EAISVCSERNO로 그룹핑하여 1행으로 집계를 한 다음 (한거래 안에서 adapter가 여러개 있을 경우 seq=100의 adapter를 기준) + * 총건수 : EAISVCSERNO로 그룹핑한 총 건수 * 성공건수 : seq = 400 and EAIERRCD = null 인 건수 * Timeout : EAIERRCD in (공통코드 CODEGROUP = 'ERRCODE_TIMEOUT') 인 건수 * 시스템오류 : (seq400 = null or EAIERRCD is not null) and 공통코드(ERRCODE_TIMEOUT)에 정의되지 않은 errorcode 인 건수 @@ -120,9 +120,8 @@ public class ApiStatsHourlyAggregationJob implements Job { " SELECT EAISVCSERNO" + " , MAX(EAISVCNAME) AS API_NAME, MAX(EAISEVRINSTNCNAME) AS GW_INSTANCE_ID" + " , MAX(EAIBZWKDSTCD) AS BIZ_DIV_CODE, MAX(CLIENTID) AS CLIENT_ID" + - " , MAX(CLIENTNAME) AS CLIENT_NAME, MAX(ORGID) AS ORG_ID, MAX(ORGNAME) AS ORG_NAME" + - " , MAX(GSTATSYSADPTRBZWKGROUPNAME) AS INBOUND_ADAPTER" + - " , MAX(PSVSYSADPTRBZWKGROUPNAME) AS OUTBOUND_ADAPTER" + + " , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN GSTATSYSADPTRBZWKGROUPNAME ELSE '' END) AS INBOUND_ADAPTER" + + " , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN PSVSYSADPTRBZWKGROUPNAME ELSE '' END) AS OUTBOUND_ADAPTER" + " , MIN(MSGDPSTYMS) AS DT" + " , MAX(MSGPRCSSYMS) - MIN(MSGDPSTYMS) AS RESP_TIME" + " , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" + diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/statistics/ApiUseStatsController.java b/src/main/java/com/eactive/eai/rms/ext/djb/statistics/ApiUseStatsController.java index 3b79e3c..8676b9f 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/statistics/ApiUseStatsController.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/statistics/ApiUseStatsController.java @@ -1,29 +1,22 @@ package com.eactive.eai.rms.ext.djb.statistics; import java.io.IOException; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.stream.Collectors; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import com.eactive.eai.rms.common.vo.GridResponse; -import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay; -import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService; -import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper; -import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService; -import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI; +import com.eactive.eai.rms.data.ext.djb.statistics.ApiUseStatsExcelExportService; +import com.eactive.eai.rms.data.ext.djb.statistics.ApiUseStatsService; import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; @@ -35,37 +28,29 @@ import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor public class ApiUseStatsController { - private final ApiStatsDayService service; - private final ApiStatsUIMapper mapper; - private final ApiStatsExcelExportService excelExportService; + private final ApiUseStatsService service; + private final ApiUseStatsExcelExportService excelExportService; - @GetMapping(value = "/onl/djb/statistics/apiUseStatsMan.view") + @GetMapping(value = "/onl/kjb/statistics/apiUseStatsMan.view") public String view() { - return "/onl/djb/statistics/apiUseStatsMan"; + return "/onl/kjb/statistics/apiUseStatsMan"; } - @PostMapping(value = "/onl/djb/statistics/apiUseStatsMan.json", params = "cmd=LIST") + @PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=LIST") public ResponseEntity> selectList(ApiStatsSearch search, Pageable pageable) { - Pageable sortedPageable = PageRequest.of( - pageable.getPageNumber(), - pageable.getPageSize(), - Sort.by(Sort.Order.desc("statTime"))); - - Page page = service.selectList(search, sortedPageable); - Page uiPage = page.map(mapper::toVo); - - return ResponseEntity.ok(new GridResponse<>(uiPage)); + Page page = service.selectList(search, pageable); + return ResponseEntity.ok(new GridResponse<>(page)); } - @PostMapping(value = "/onl/djb/statistics/apiStatsDayMan.json", params = "cmd=EXCEL_EXPORT") + @PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=EXCEL_EXPORT") public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException { log.info("Excel export started - search: {}", search); try { - List dataList = service.selectListForExcel(search); - log.info("Data retrieved - count: {}", dataList.size()); + List uiList = service.selectList(search); + log.info("Data retrieved - count: {}", uiList.size()); - if (dataList.isEmpty()) { + if (uiList.isEmpty()) { log.warn("No data found for export"); response.setStatus(HttpServletResponse.SC_NO_CONTENT); response.setContentType("application/json; charset=UTF-8"); @@ -74,11 +59,6 @@ public class ApiUseStatsController { return; } - List uiList = dataList.stream() - .map(mapper::toVo) - .collect(Collectors.toList()); - log.info("Data converted to UI list - count: {}", uiList.size()); - String fileName = generateFileName(search); log.info("Generating Excel file: {}", fileName); @@ -101,10 +81,7 @@ public class ApiUseStatsController { } private String generateFileName(ApiStatsSearch search) { - String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime()) - ? search.getSearchStartDateTime() - : LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - return "API사용현황_" + timeRange + ".xlsx"; + return "API사용현황_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx"; } } diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java index 96c0e58..2f354fd 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsSearch.java @@ -18,6 +18,8 @@ public class ApiStatsSearch { private String searchClientId; private String searchInboundAdapter; private String searchOutboundAdapter; + private String searchOrgName; + private String searchType; /** 파싱된 시작시간 */ private LocalDateTime parsedStartDateTime; diff --git a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java index 3a57ab9..83ee405 100644 --- a/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java +++ b/src/main/java/com/eactive/ext/kjb/statistics/ui/ApiStatsUI.java @@ -14,6 +14,7 @@ public class ApiStatsUI { private String gwInstanceId; private String bizDivCode; private String clientId; + private String orgName; private String inboundAdapter; private String outboundAdapter; @@ -27,4 +28,8 @@ public class ApiStatsUI { private BigDecimal avgRespTime; private BigDecimal minRespTime; private BigDecimal maxRespTime; + + //성공율,실패율 + private BigDecimal successRate; + private BigDecimal failRate; } From 191857811fd21e01a27f7df0d83597df974497d3 Mon Sep 17 00:00:00 2001 From: eastargh Date: Wed, 20 May 2026 11:50:40 +0900 Subject: [PATCH 09/45] =?UTF-8?q?=EA=B3=B5=EC=A7=80=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=B2=A8=EB=B6=80=ED=8C=8C=EC=9D=BC=20=EC=95=88=EC=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apim/portalnotice/portalNoticeManDetail.jsp | 4 ++-- .../eactive/eai/rms/common/util/StringUtils.java | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/WebContent/jsp/onl/apim/portalnotice/portalNoticeManDetail.jsp b/WebContent/jsp/onl/apim/portalnotice/portalNoticeManDetail.jsp index c26dad6..6e74a8d 100644 --- a/WebContent/jsp/onl/apim/portalnotice/portalNoticeManDetail.jsp +++ b/WebContent/jsp/onl/apim/portalnotice/portalNoticeManDetail.jsp @@ -297,7 +297,7 @@ - + diff --git a/src/main/java/com/eactive/eai/rms/common/util/StringUtils.java b/src/main/java/com/eactive/eai/rms/common/util/StringUtils.java index 2376dd1..a6f02c1 100644 --- a/src/main/java/com/eactive/eai/rms/common/util/StringUtils.java +++ b/src/main/java/com/eactive/eai/rms/common/util/StringUtils.java @@ -1,5 +1,6 @@ package com.eactive.eai.rms.common.util; +import java.math.BigDecimal; import java.util.List; import org.apache.log4j.Logger; @@ -191,6 +192,20 @@ public class StringUtils } return sb.toString(); } + + public static String toString(Object val) { + return val != null ? val.toString() : null; + } + + public static Long toLong(Object val) { + return val != null ? ((Number) val).longValue() : null; + } + + public static BigDecimal toDecimal(Object val) { + if (val == null) return null; + if (val instanceof BigDecimal) return (BigDecimal) val; + return new BigDecimal(val.toString()); + } // // /** // * - "" 나 Null 을 입력받아 SPACE 로 변환한다. From 90c43ea7b0b67be05548b8f6303e3f5035aaec43 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Thu, 21 May 2026 09:51:39 +0900 Subject: [PATCH 10/45] =?UTF-8?q?-=20K8sUtil=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20-=20=EC=8A=B9=EC=9D=B8=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EC=99=84=EC=A0=84=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20DJBank=EB=A1=9C=20=EB=AA=85=EC=B9=AD?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + CLAUDE.md | 64 ++--- .../onl/apim/approval/portalApprovalMan.jsp | 55 +++++ build.gradle | 16 +- readme.md | 224 ++---------------- .../approval/PortalApprovalAuthorizer.java | 3 +- .../approval/PortalApprovalManController.java | 52 +++- .../approval/PortalApprovalManService.java | 33 +++ .../app/PortalAppApprovalListener.java | 27 +-- .../apim/portalorg/PortalOrgManService.java | 37 ++- .../eai/rms/onl/common/util/K8sUtil.java | 79 ------ ..._20260506_eapim-admin_feature-crypto-module.md | 2 +- 12 files changed, 247 insertions(+), 348 deletions(-) delete mode 100644 src/main/java/com/eactive/eai/rms/onl/common/util/K8sUtil.java diff --git a/.gitignore b/.gitignore index 41a9225..67a1cbc 100644 --- a/.gitignore +++ b/.gitignore @@ -246,3 +246,6 @@ eapim-admin-kjb/ *.report.md *.claude.md tmpclaude-*-cwd +logs +rinjae_ +tomcat-base \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index e51bbbb..7e15c4b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ AI 어시스턴트가 작업을 시작하기 전에 반드시 이해해야 할 - **신규 기능**: 항상 **JPA**와 Spring Data 리포지토리를 사용해야 합니다. - **레거시 코드**: 기존 iBATIS (`*.xml` 매퍼) 코드를 수정할 때는 해당 패턴을 따라야 합니다. - **멀티 모듈 Gradle 프로젝트**: `settings.gradle`에 정의된 것처럼 여러 하위 모듈(`elink-online-*`, `kjb-*` 등)로 구성되어 있습니다. 특정 기능은 다른 모듈에 위치할 수 있습니다. -- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `kjb-gradle.sh compileJava`를 먼저 실행하여 코드를 생성해야 합니다. +- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `./gradlew compileJava` 를 먼저 실행하여 코드를 생성해야 합니다. - **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다. ## 프로젝트 개요 @@ -39,50 +39,48 @@ eLink EMS (eLink Management System)는 eLink의 웹 기반 관리 서비스로, ### 빌드 명령어 -**⚠️ 중요**: 환경변수 문제로 인해 `gradle` 대신 `kjb-gradle.sh` 스크립트를 사용해야 합니다. -- 스크립트 위치: `/c/eactive/workspaces/shell-scripts/kjb-gradle.sh` -- PATH에 등록되어 있으므로 바로 `kjb-gradle.sh` 명령어 사용 가능 +프로젝트 루트의 Gradle wrapper(`./gradlew`)를 사용합니다. JDK 8 toolchain 위치는 `.envrc` 등으로 잡습니다(아래 "기술 스택" 섹션 참조). ```bash # 표준 빌드 -kjb-gradle.sh build +./gradlew build # Weblogic 배포용 빌드 (테스트 제외) -kjb-gradle.sh build -x test -Pprofile=weblogic +./gradlew build -x test -Pprofile=weblogic # WAR 파일 빌드 -kjb-gradle.sh war +./gradlew war # 클린 빌드 -kjb-gradle.sh clean build +./gradlew clean build ``` ### 테스트 실행 ```bash # 모든 테스트 실행 -kjb-gradle.sh test +./gradlew test # 특정 테스트 클래스 실행 -kjb-gradle.sh test --tests "com.example.ClassName" +./gradlew test --tests "com.example.ClassName" # 특정 테스트 패키지 실행 (JUnit 플랫폼 사용) -kjb-gradle.sh test --tests "com.eactive.eai.rms.*" +./gradlew test --tests "com.eactive.eai.rms.*" ``` ### 개발 태스크 ```bash # 패키징 없이 클래스만 컴파일 -kjb-gradle.sh classes +./gradlew classes # QueryDSL Q-classes 및 기타 애노테이션 생성 -kjb-gradle.sh compileJava +./gradlew compileJava # 의존성 트리 보기 -kjb-gradle.sh dependencies +./gradlew dependencies # 사용 가능한 모든 태스크 목록 -kjb-gradle.sh tasks --all +./gradlew tasks --all ``` ## 로컬 개발 빠른 시작 @@ -92,26 +90,26 @@ kjb-gradle.sh tasks --all 1. **IDE 설정 파일 생성 (최초 1회)** ```bash # Eclipse 사용 시 (기본) - kjb-gradle.sh eclipse + ./gradlew eclipse # IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요) # 1. build.gradle을 build.gradle.eclipse로 백업 # 2. build.gradle.intellij를 build.gradle로 복사 - # 3. kjb-gradle.sh idea 실행 + # 3. ./gradlew idea 실행 ``` 2. **QueryDSL 등 소스 코드 생성** ```bash - kjb-gradle.sh compileJava + ./gradlew compileJava ``` 3. **전체 빌드 및 테스트 실행** ```bash - kjb-gradle.sh build + ./gradlew build ``` 4. **IDE에서 프로젝트 열기** - - Eclipse: `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다. + - Eclipse: `./gradlew eclipse` 실행 후, IDE에서 프로젝트를 엽니다. - IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다. 5. **실행 구성(Run Configuration) 설정** @@ -162,14 +160,14 @@ eapim-admin (root project) #### Q-Class 생성 위치 Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다. -- `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 자동으로 생성 +- `./gradlew compileJava` 또는 `./gradlew build` 실행 시 Gradle이 자동으로 생성 - 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다 #### 생성된 코드 작업하기 - **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다 -- **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하여 Q-classes를 재생성합니다 -- **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava`로 재생성합니다 +- **업데이트 pull 후**: `./gradlew compileJava` 를 실행하여 Q-classes를 재생성합니다 +- **Q-classes IDE 오류 시**: `./gradlew clean compileJava` 로 재생성합니다 #### IDE별 빌드 설정 파일 @@ -255,12 +253,21 @@ IntelliJ를 사용하려면 `build.gradle.intellij`를 `build.gradle`로 교체 WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다: ```bash -kjb-gradle.sh build -x test -Pprofile=weblogic +./gradlew build -x test -Pprofile=weblogic ``` ## 기술 스택 -- **Java 8**: Gradle toolchain을 통해 강제 +- **Java 8**: Gradle toolchain을 통해 강제 (`JavaLanguageVersion.of(8)`). 프로젝트의 `gradle.properties` 에는 JDK 경로를 기입하지 않습니다 — 개인 환경별로 `.envrc`(direnv) 또는 셸 rc 파일에서 `JAVA_HOME` 을 JDK 8 위치로 export 하세요. Gradle 8 의 toolchain auto-detect 가 `JAVA_HOME` 도 후보 경로로 인식합니다. + + ```bash + # 예: 프로젝트 루트의 .envrc (Zulu 8 / macOS Apple Silicon) + JAVA_HOME=${HOME}/opts/jdks/zulu8.94.0.17-ca-jdk8.0.492-macosx_aarch64 + GRADLE_USER_HOME=${HOME}/eactive/djb-eapim/gradle-home + export PATH=${JAVA_HOME}/bin:${PATH} + ``` + + Gradle 표준 위치(`/Library/Java/JavaVirtualMachines/...`, Homebrew, SDKMAN 등)에 설치한 경우는 별도 설정 없이도 auto-detect 됩니다. - **Spring Framework 5.3.27**: 코어 프레임워크 (MVC, Data JPA) - **Spring Data JPA 2.5.2**: 리포지토리 추상화 - **Hibernate 5.4.33/5.6.15**: ORM 구현 (JPA/ORM) @@ -271,9 +278,10 @@ kjb-gradle.sh build -x test -Pprofile=weblogic - **Jackson 2.13.1**: JSON 처리 - **Logback 1.2.10**: 로깅 프레임워크 (SLF4J와 함께) - **AWS SDK 2.20.142**: S3 연동 -- **Kubernetes Client 18.0.1**: K8s 관리 - **EhCache**: 캐싱, **Lombok**: 코드 생성, **MapStruct**: 객체 매핑 +> Kubernetes Client 의존성은 JDK 8 호환 문제로 제거됨(`io.kubernetes:client-java:18.0.1` 은 JDK 11+ 필요). K8s 모드 기능이 필요하면 `≤13.0.2` 로 다운그레이드 후 재도입 검토. + ## 프로젝트 규칙 ### 패키지 구조 @@ -376,7 +384,7 @@ com.eactive.eai ### IntelliJ IDEA (권장) ```bash -kjb-gradle.sh idea +./gradlew idea ``` 또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다. @@ -395,7 +403,7 @@ cp build.gradle build.gradle.intellij cp build.gradle.eclipse build.gradle # 3. Eclipse 프로젝트 생성 -kjb-gradle.sh eclipse +./gradlew eclipse ``` Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다: diff --git a/WebContent/jsp/onl/apim/approval/portalApprovalMan.jsp b/WebContent/jsp/onl/apim/approval/portalApprovalMan.jsp index dff4b54..880c94a 100644 --- a/WebContent/jsp/onl/apim/approval/portalApprovalMan.jsp +++ b/WebContent/jsp/onl/apim/approval/portalApprovalMan.jsp @@ -17,11 +17,52 @@ + <%-- --%> diff --git a/WebContent/jsp/common/screen/main.jsp b/WebContent/jsp/common/screen/main.jsp index 0642401..8748ef8 100644 --- a/WebContent/jsp/common/screen/main.jsp +++ b/WebContent/jsp/common/screen/main.jsp @@ -99,6 +99,93 @@ - + + <%-- 자동 로그아웃 경고 모달 --%> + + + + diff --git a/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java b/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java index f274bf3..e9d6ed9 100644 --- a/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java +++ b/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java @@ -255,6 +255,9 @@ public interface MonitoringContext { // 이중 로그인 허용여부 public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED"; + // 자동 로그아웃 타임아웃 (단위: 분, 기본값: 10) + public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout"; + // 웹훅 재전송 설정 public static final String API_WEBHOOK_RETRY_COUNT = "api.webhook.retry_count"; public static final String API_WEBHOOK_RETRY_TIME = "api.webhook.retry_time"; diff --git a/src/main/java/com/eactive/eai/rms/common/login/MainController.java b/src/main/java/com/eactive/eai/rms/common/login/MainController.java index 1471ec8..3994a19 100644 --- a/src/main/java/com/eactive/eai/rms/common/login/MainController.java +++ b/src/main/java/com/eactive/eai/rms/common/login/MainController.java @@ -617,6 +617,8 @@ public class MainController implements InterceptorSkipController { menuId = menuId == null ? "" : menuId; model.addAttribute("mainPage", mainPage); model.addAttribute("menuId", menuId); + model.addAttribute("autoLogoutTimeout", + monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 10)); if (logger.isDebugEnabled()) { logger.debug("CURRENT SERVICE TYPE : " + service); From feb4f845a840f43c923c764b7c7090b778c0d994 Mon Sep 17 00:00:00 2001 From: eastargh Date: Fri, 5 Jun 2026 10:11:21 +0900 Subject: [PATCH 20/45] =?UTF-8?q?=ED=8C=9D=EC=97=85=20=EB=91=90=EB=B2=88?= =?UTF-8?q?=EC=A7=B8=20=EC=9D=B4=ED=9B=84=20=EC=98=A4=ED=94=88=EC=8B=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebContent/js/custom.js | 8 ++++++++ WebContent/jsp/js/custom.js | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/WebContent/js/custom.js b/WebContent/js/custom.js index e6bb511..c5e7c5f 100644 --- a/WebContent/js/custom.js +++ b/WebContent/js/custom.js @@ -36,6 +36,14 @@ // save the returnValue in options so that it is available in the callback function optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.onClose(); + // Remove iframe from DOM so window.frames doesn't accumulate stale entries; + // without this, a second showModal call leaves frames[0] pointing to the first + // (closed) iframe, causing the previous returnValue to bleed into the next callback. + var $dialogEl = $frame.closest('.ui-dialog'); + setTimeout(function() { + $frame.remove(); + if ($dialogEl.length) $dialogEl.remove(); + }, 0); if(optns.option && optns.option.fullScreenPopup){ $('iframe[name=topFrame]',window.parent.document).css('display', 'block'); diff --git a/WebContent/jsp/js/custom.js b/WebContent/jsp/js/custom.js index 2cde935..5eda123 100644 --- a/WebContent/jsp/js/custom.js +++ b/WebContent/jsp/js/custom.js @@ -36,6 +36,14 @@ // save the returnValue in options so that it is available in the callback function optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.onClose(); + // Remove iframe from DOM so window.frames doesn't accumulate stale entries; + // without this, a second showModal call leaves frames[0] pointing to the first + // (closed) iframe, causing the previous returnValue to bleed into the next callback. + var $dialogEl = $frame.closest('.ui-dialog'); + setTimeout(function() { + $frame.remove(); + if ($dialogEl.length) $dialogEl.remove(); + }, 0); if(optns.option && optns.option.fullScreenPopup){ $('iframe[name=topFrame]',window.parent.document).css('display', 'block'); From 480d7dac5a54b63dc6074144bf2bd97e98214d63 Mon Sep 17 00:00:00 2001 From: eastargh Date: Fri, 5 Jun 2026 15:10:06 +0900 Subject: [PATCH 21/45] =?UTF-8?q?=EC=95=B1=EC=8A=B9=EC=9D=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=98=88=EC=83=81=EC=99=84=EB=A3=8C=EC=9D=BC=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=EC=B6=94=EA=B0=80=20&=20=EC=8A=B9=EC=9D=B8?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=8B=9C=20"=EC=A7=84=ED=96=89=EC=A4=91"=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../approval/portalAppApprovalManPopup.jsp | 57 ++++++++++++++++++- .../onl/apim/approval/portalApprovalMan.jsp | 15 ++++- .../approval/PortalApprovalManController.java | 6 ++ .../approval/PortalApprovalManService.java | 17 ++++++ .../onl/apim/approval/PortalApprovalUI.java | 2 + 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/WebContent/jsp/onl/apim/approval/portalAppApprovalManPopup.jsp b/WebContent/jsp/onl/apim/approval/portalAppApprovalManPopup.jsp index ad28250..03e3996 100644 --- a/WebContent/jsp/onl/apim/approval/portalAppApprovalManPopup.jsp +++ b/WebContent/jsp/onl/apim/approval/portalAppApprovalManPopup.jsp @@ -38,12 +38,22 @@ var loginUser = '<%= loginVo.getUserId() %>'; var url = ''; var url_view = ''; + + function dateFormat(cellvalue) { + if ( cellvalue == null || cellvalue.length < 8 ) + return ''; + return cellvalue.substr(0 ,4) + '-' + + cellvalue.substr(4, 2) + '-' + + cellvalue.substr(6, 2) ; + } $(document).ready(function () { var key = "${param.id}"; if (key) { detail(key); } + + $("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true}); $("#btn_app_approve").click(function () { if (confirm("승인하시겠습니까?")) { @@ -112,6 +122,28 @@ }); } }); + + $("#btn_modify").click(function () { + if (confirm("수정하시겠습니까?")) { + $.ajax({ + type: "POST", + url: url, + data: {cmd: "UPDATE", id: key, expectEndDate: $("#expectEndDate").val().replaceAll('-','')}, + success: function (args) { + alert("수정되었습니다."); + if (window.dialogArguments && window.dialogArguments.self) { + window.dialogArguments.self.location.reload(); + } else if (window.opener) { + window.opener.location.reload(); + } + window.close(); + }, + error: function (e) { + alert(e.responseText); + } + }); + } + }); $("#btn_close").click(function () { window.close(); @@ -126,12 +158,25 @@ data: {cmd: 'DETAIL', id: key}, success: function (json) { var data = json; + + if (data.approvalStatus.code == 'END') { + $('#btn_modify').hide(); + $('#expectEndDate').hide(); + $('#spExpectEndDate').show(); + $('#spExpectEndDate').text(dateFormat(data.expectEndDate)); + } else { + $('#btn_modify').show(); + $('#expectEndDate').show(); + $('#spExpectEndDate').hide(); + } + $("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용"); $("#approvalStatus").text(data.approvalStatus.description); $("#requesterName").text(data.requester.userName); $("#approvalSubject").text(data.approvalSubject); $("#approvalDetail").html(data.approvalDetail); $("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true}); + $("#expectEndDate").val(data.expectEndDate); if (data.approvers.some(approver => approver.approverStatus === 'CURRENT' && @@ -144,7 +189,7 @@ document.getElementById("btn_app_reject").style.display = "none"; } - if (data.approvalStatus.description === '배포실패') { + if (data.approvalStatus.code === 'FAILED') { document.getElementById("btn_app_redeploy").style.display = ""; } @@ -242,9 +287,14 @@ 상세 내용 - +
+ 예상완료일 + + + + @@ -282,6 +332,9 @@
@@ -270,9 +357,9 @@ - 게시유형 - -
+ 게시유형 * + +
@@ -286,7 +373,7 @@ 사용 - 고정여부 + 상단고정 고정 @@ -297,19 +384,58 @@ - + + 시작 * + + - --> + 종료 + + + + + + 상태 + +
+ +
+ + 이전 상태 + 없음 + + + 영향 API 선택 + +
+ + +
+ + + + + + + + + + + +
API IDAPI 명
선택된 영향 API가 없습니다.
+ +
- \ No newline at end of file + diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/IncidentAffectedApiUI.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/IncidentAffectedApiUI.java new file mode 100644 index 0000000..d38dcc7 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/IncidentAffectedApiUI.java @@ -0,0 +1,11 @@ +package com.eactive.eai.rms.onl.apim.portalnotice; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class IncidentAffectedApiUI { + private String apiId; + private String apiName; +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeManService.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeManService.java index 09bf0ad..3ec890b 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeManService.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeManService.java @@ -1,5 +1,13 @@ package com.eactive.eai.rms.onl.apim.portalnotice; +import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident; +import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApi; +import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApiId; +import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind; +import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentState; +import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository; +import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository; +import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentTimelineRepository; import com.eactive.apim.portal.file.entity.FileDetail; import com.eactive.apim.portal.file.entity.FileInfo; import com.eactive.apim.portal.file.service.FileService; @@ -23,7 +31,12 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Service @Transactional(transactionManager = "transactionManagerForEMS") @@ -32,6 +45,9 @@ public class PortalNoticeManService extends BaseService { private final PortalNoticeService portalNoticeService; private final PortalNoticeUIMapper portalNoticeUIMapper; private final FileService fileService; + private final DjbApistatusIncidentRepository incidentRepository; + private final DjbApistatusIncidentApiRepository incidentApiRepository; + private final DjbApistatusIncidentTimelineRepository incidentTimelineRepository; private String decodeString(String value) { if (ContainerUtil.get() == ContainerUtil.TOMCAT) { @@ -48,10 +64,27 @@ public class PortalNoticeManService extends BaseService { @Autowired public PortalNoticeManService(PortalNoticeService portalNoticeService, PortalNoticeUIMapper portalNoticeUIMapper, - FileService fileService){ + FileService fileService, + DjbApistatusIncidentRepository incidentRepository, + DjbApistatusIncidentApiRepository incidentApiRepository, + DjbApistatusIncidentTimelineRepository incidentTimelineRepository){ this.portalNoticeService = portalNoticeService; this.portalNoticeUIMapper = portalNoticeUIMapper; this.fileService = fileService; + this.incidentRepository = incidentRepository; + this.incidentApiRepository = incidentApiRepository; + this.incidentTimelineRepository = incidentTimelineRepository; + } + + private static boolean isIncidentKind(String noticeType) { + return PortalNoticeUI.NOTICE_TYPE_INCIDENT.equals(noticeType) + || PortalNoticeUI.NOTICE_TYPE_MAINTENANCE.equals(noticeType); + } + + private static IncidentKind toKind(String noticeType) { + if (PortalNoticeUI.NOTICE_TYPE_INCIDENT.equals(noticeType)) return IncidentKind.INCIDENT; + if (PortalNoticeUI.NOTICE_TYPE_MAINTENANCE.equals(noticeType)) return IncidentKind.MAINTENANCE; + return null; } @@ -89,9 +122,39 @@ public class PortalNoticeManService extends BaseService { PortalNoticeUI ui = portalNoticeUIMapper.toVo(portalNotice); setFileInfo(portalNotice, ui); + populateIncident(portalNotice, ui); return ui; } + private void populateIncident(PortalNotice portalNotice, PortalNoticeUI ui) { + if (!isIncidentKind(portalNotice.getNoticeType())) { + ui.setAffectedApis(Collections.emptyList()); + return; + } + Optional incidentOpt = incidentRepository.findByNoticeId(portalNotice.getId()); + if (!incidentOpt.isPresent()) { + ui.setAffectedApis(Collections.emptyList()); + return; + } + DjbApistatusIncident incident = incidentOpt.get(); + ui.setIncidentId(incident.getIncidentId()); + ui.setSummary(incident.getSummary()); + ui.setStartedAt(incident.getStartedAt()); + ui.setEndAt(incident.getEndAt()); + ui.setState(incident.getState() == null ? null : incident.getState().name()); + ui.setPreviousState(incident.getPreviousState() == null ? null : incident.getPreviousState().name()); + + List apis = incidentApiRepository + .findByIncidentIdOrderByApiId(incident.getIncidentId()).stream() + .map(api -> { + IncidentAffectedApiUI vo = new IncidentAffectedApiUI(); + vo.setApiId(api.getApiId()); + vo.setApiName(api.getApiName()); + return vo; + }).collect(Collectors.toList()); + ui.setAffectedApis(apis); + } + public void insert(PortalNoticeUI portalNoticeUI) throws IOException { //portalNoticeUI.setNoticeSubject(decodeString(portalNoticeUI.getNoticeSubject())); //portalNoticeUI.setNoticeDetail(decodeString(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail()))); @@ -106,6 +169,61 @@ public class PortalNoticeManService extends BaseService { } portalNoticeService.save(portalNotice); + + if (isIncidentKind(portalNoticeUI.getNoticeType())) { + persistIncident(portalNoticeUI, portalNotice, true); + } + } + + private void persistIncident(PortalNoticeUI portalNoticeUI, PortalNotice portalNotice, boolean isCreate) { + IncidentKind kind = toKind(portalNoticeUI.getNoticeType()); + + DjbApistatusIncident incident = incidentRepository.findByNoticeId(portalNotice.getId()) + .orElseGet(DjbApistatusIncident::new); + + incident.setKind(kind); + incident.setTitle(portalNotice.getNoticeSubject()); + incident.setSummary(portalNoticeUI.getSummary()); + incident.setStartedAt(portalNoticeUI.getStartedAt() != null + ? portalNoticeUI.getStartedAt() : LocalDateTime.now()); + incident.setEndAt(portalNoticeUI.getEndAt()); + incident.setDetectedBy("MANUAL"); + incident.setNoticeId(portalNotice.getId()); + incident.setDraftYn("N"); + incident.setFixYn(StringUtils.defaultIfBlank(portalNotice.getFixYn(), "N")); + + if (kind == IncidentKind.INCIDENT) { + IncidentState newState = StringUtils.isNotBlank(portalNoticeUI.getState()) + ? IncidentState.valueOf(portalNoticeUI.getState()) + : IncidentState.INVESTIGATING; + if (!isCreate && incident.getState() != newState) { + incident.setPreviousState(incident.getState()); + } + incident.setState(newState); + } else { + incident.setState(null); + incident.setPreviousState(null); + } + + DjbApistatusIncident saved = incidentRepository.save(incident); + + // 영향 API 동기화 — 단순 전체 삭제 후 재삽입 + incidentApiRepository.deleteByIncidentId(saved.getIncidentId()); + List affected = portalNoticeUI.getAffectedApis(); + if (affected != null && !affected.isEmpty()) { + List rows = new ArrayList<>(); + for (IncidentAffectedApiUI src : affected) { + if (StringUtils.isBlank(src.getApiId())) continue; + DjbApistatusIncidentApi api = new DjbApistatusIncidentApi(); + api.setIncidentId(saved.getIncidentId()); + api.setApiId(src.getApiId()); + api.setApiName(src.getApiName()); + rows.add(api); + } + if (!rows.isEmpty()) { + incidentApiRepository.saveAll(rows); + } + } } private void handleFileUpload(PortalNoticeUI portalNoticeUI, PortalNotice portalNotice) throws IOException { @@ -129,6 +247,7 @@ public class PortalNoticeManService extends BaseService { portalNoticeUI.setNoticeDetail(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail())); PortalNotice portalNotice = portalNoticeService.getById(portalNoticeUI.getId()); + String previousNoticeType = portalNotice.getNoticeType(); if (portalNoticeUI.getFiles() != null && !portalNoticeUI.getFiles().isEmpty()) { handleFileUpload(portalNoticeUI, portalNotice); @@ -139,11 +258,27 @@ public class PortalNoticeManService extends BaseService { portalNoticeUIMapper.updateToEntity(portalNoticeUI, portalNotice); portalNoticeService.save(portalNotice); + + if (isIncidentKind(portalNotice.getNoticeType())) { + persistIncident(portalNoticeUI, portalNotice, false); + } else if (isIncidentKind(previousNoticeType)) { + // 일반 공지로 유형 변경 → 기존 incident 정리 + deleteIncidentIfExists(portalNotice.getId()); + } + } + + private void deleteIncidentIfExists(String noticeId) { + incidentRepository.findByNoticeId(noticeId).ifPresent(incident -> { + incidentApiRepository.deleteByIncidentId(incident.getIncidentId()); + incidentTimelineRepository.deleteByIncidentId(incident.getIncidentId()); + incidentRepository.delete(incident); + }); } public void delete(String id) { PortalNotice portalNotice = portalNoticeService.getById(id); Optional.ofNullable(portalNotice.getFileId()).ifPresent(fileService::deleteFile); + deleteIncidentIfExists(id); portalNoticeService.deleteById(id); } diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeUI.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeUI.java index bded835..db2a1da 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeUI.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeUI.java @@ -8,11 +8,16 @@ import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; +import java.util.List; @Data public class PortalNoticeUI { + public static final String NOTICE_TYPE_NORMAL = "1"; + public static final String NOTICE_TYPE_MAINTENANCE = "2"; + public static final String NOTICE_TYPE_INCIDENT = "3"; + private String id; private String noticeSubject; @@ -53,4 +58,24 @@ public class PortalNoticeUI { private boolean hasAttachment; // 첨부파일 여부 private boolean fileDeleted; // 파일 삭제 여부 + + // ───── 장애/점검(djb_apistatus_incident) 연동 필드 ───── + private Long incidentId; + + private String summary; + + // HTML 는 'T' 구분자 형식 (예: 2026-06-16T10:29). + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") + private LocalDateTime startedAt; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm") + private LocalDateTime endAt; + + private String state; // INCIDENT 한정 (INVESTIGATING/IDENTIFIED/MONITORING/RESOLVED/CANCELED) + + private String previousState; // 직전 상태 + + private List affectedApis; } diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeUIMapper.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeUIMapper.java index d8fa0ed..02aa8c1 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeUIMapper.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalnotice/PortalNoticeUIMapper.java @@ -3,7 +3,9 @@ package com.eactive.eai.rms.onl.apim.portalnotice; import com.eactive.apim.portal.portalNotice.entity.PortalNotice; import org.mapstruct.*; -@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +@Mapper(componentModel = "spring", + unmappedTargetPolicy = ReportingPolicy.IGNORE, + unmappedSourcePolicy = ReportingPolicy.IGNORE) public interface PortalNoticeUIMapper { PortalNoticeUI toVo(PortalNotice entity); From 58fe176715b2027f783a2dae98e1583beaf34c8c Mon Sep 17 00:00:00 2001 From: eastargh Date: Tue, 16 Jun 2026 15:05:48 +0900 Subject: [PATCH 42/45] =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=EC=88=9C=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebContent/jsp/onl/admin/service/stdDefaultMan.jsp | 14 ++++++++------ .../ExtendedColumnDefinitionService.java | 9 ++++++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/WebContent/jsp/onl/admin/service/stdDefaultMan.jsp b/WebContent/jsp/onl/admin/service/stdDefaultMan.jsp index afa60f5..f9d8d57 100644 --- a/WebContent/jsp/onl/admin/service/stdDefaultMan.jsp +++ b/WebContent/jsp/onl/admin/service/stdDefaultMan.jsp @@ -44,14 +44,16 @@ '<%=localeMessage.getString("stdDefaultMan.columDesc")%>', '<%=localeMessage.getString("stdDefaultMan.columLen")%>', '<%=localeMessage.getString("stdDefaultMan.keyOr")%>', - '<%=localeMessage.getString("stdDefaultMan.ifOr")%>' + '<%=localeMessage.getString("stdDefaultMan.ifOr")%>', + '<%=localeMessage.getString("stdDefaultMan.orderSeq")%>' ], colModel:[ - { name : 'COLUMNNAME' , align:'left' , sortable:false }, - { name : 'COLUMNDESC' , align:'left' }, - { name : 'COLUMNLENGTH' , align:'right' }, - { name : 'ISKEY' , align:'center' }, - { name : 'ISINTERFACE' , align:'center' } + { name : 'COLUMNNAME' , align:'left' , width: '200px', sortable:false }, + { name : 'COLUMNDESC' , align:'left' , width: '200px' }, + { name : 'COLUMNLENGTH' , align:'center' , width: '100px' }, + { name : 'ISKEY' , align:'center', width: '100px' }, + { name : 'ISINTERFACE' , align:'center', width: '100px' }, + { name : 'ORDERSEQ' , align:'center', width: '100px' } ], jsonReader: { repeatitems:false diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/stdmessage/ExtendedColumnDefinitionService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/stdmessage/ExtendedColumnDefinitionService.java index 5567d62..2b44c59 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/stdmessage/ExtendedColumnDefinitionService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/stdmessage/ExtendedColumnDefinitionService.java @@ -5,7 +5,9 @@ import java.util.List; 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,7 +44,12 @@ public class ExtendedColumnDefinitionService predicate = predicate.and(qExtendedColumnDefinition.columnDesc.containsIgnoreCase(searchColumnDesc)); } - return repository.findAll(predicate, pageable); + Pageable orderedPageable = PageRequest.of( + pageable.getPageNumber(), + pageable.getPageSize(), + Sort.by(Sort.Order.asc("orderSeq"), Sort.Order.asc("columnName"))); + + return repository.findAll(predicate, orderedPageable); } } From 6ab7e1fe16d80659d05f765a7b230f70e801ece7 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 16 Jun 2026 15:48:30 +0900 Subject: [PATCH 43/45] =?UTF-8?q?ci=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/eapim-admin-ci.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.gitea/workflows/eapim-admin-ci.yml b/.gitea/workflows/eapim-admin-ci.yml index c0d7b08..08d0152 100644 --- a/.gitea/workflows/eapim-admin-ci.yml +++ b/.gitea/workflows/eapim-admin-ci.yml @@ -65,14 +65,6 @@ jobs: path: eapim-admin/build/libs/eapim-admin.war if-no-files-found: error - - name: Sync systemd user unit - working-directory: ${{ github.workspace }} - run: | - install -D -m 644 eapim-admin/deploy/systemd/eapim-admin.service \ - "$HOME/.config/systemd/user/eapim-admin.service" - systemctl --user daemon-reload - systemctl --user enable eapim-admin 2>/dev/null || true - - name: Stop tomcat (pre-clean) working-directory: ${{ github.workspace }} run: | From df5bbb162a9b1a2a35c05c9c9a06a1f7de6aaf47 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 16 Jun 2026 15:56:46 +0900 Subject: [PATCH 44/45] =?UTF-8?q?ci=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/eapim-admin-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/eapim-admin-ci.yml b/.gitea/workflows/eapim-admin-ci.yml index 08d0152..eba94c0 100644 --- a/.gitea/workflows/eapim-admin-ci.yml +++ b/.gitea/workflows/eapim-admin-ci.yml @@ -89,7 +89,7 @@ jobs: cp eapim-admin/build/libs/eapim-admin.war "$DEPLOY_DIR/monitoring.war" ls -la "$DEPLOY_DIR" - - name: Start tomcat and readiness probe (timeout 120s) + - name: Start tomcat and readiness probe (timeout 180s) working-directory: ${{ github.workspace }} run: | LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log" @@ -99,7 +99,7 @@ jobs: # systemd 가 Tomcat 을 새 cgroup 으로 띄움 — runner step 종료와 무관하게 살아남음 systemctl --user start eapim-admin - DEADLINE=$(($(date +%s) + 120)) + DEADLINE=$(($(date +%s) + 180)) STATUS=000 while [ $(date +%s) -lt $DEADLINE ]; do STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \ @@ -122,7 +122,7 @@ jobs: case "$STATUS" in 200|302|401) ;; *) - echo "::error::Readiness probe failed within 120s (last HTTP status: $STATUS)" + echo "::error::Readiness probe failed within 180s (last HTTP status: $STATUS)" [ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -100 exit 1 ;; From a440ac842d1eeeccab0fcc44a2de92aff07282fc Mon Sep 17 00:00:00 2001 From: eastargh Date: Tue, 16 Jun 2026 16:05:44 +0900 Subject: [PATCH 45/45] =?UTF-8?q?API=EC=83=81=ED=83=9C=20=EB=B3=80?= =?UTF-8?q?=ED=99=94=20=EA=B0=90=EC=A7=80=20(=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=ED=8C=90=20=EC=A0=81=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eai/rms/ext/djb/apistatus/ApiStatusRepository.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java index 617077b..750d3f7 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusRepository.java @@ -39,11 +39,10 @@ public interface ApiStatusRepository extends BaseRepository { + " FROM (" + " SELECT A.EAISVCNAME" + " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE" - //TODO: 게시판 임시로 생성하여 사용. 유차장이 완료하면 변경할것 - + " , NVL(( SELECT 'Y' FROM PTL_NOTICE_API X " - + " WHERE EXISTS (SELECT 1 FROM EMSAPP.PTL_NOTICE Y WHERE X.NOTICE_ID = Y.ID)" - //+ " AND TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI') BETWEEN START_TIME AND END_TIME) " - + " AND A.EAISVCNAME = X.API_NAME),'N') AS CTRL_YN " + + " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X" + + " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID " + + " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) " + + " AND A.EAISVCNAME = X.API_ID),'N') AS CTRL_YN " + " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'" + " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'" + " ELSE 'N' END"