From c3ad216ac9656709d6db3c409aadfb81196b08b5 Mon Sep 17 00:00:00 2001 From: eastargh Date: Thu, 17 Sep 2026 10:39:53 +0900 Subject: [PATCH] =?UTF-8?q?API=EC=83=81=ED=83=9C=EB=B3=80=ED=99=94?= =?UTF-8?q?=EC=8B=9C=20=EC=8A=A4=EC=9C=99=EC=95=8C=EB=A6=BC=EC=97=90=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A9=94=EC=84=B8=EC=A7=80,=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=A1=B0=ED=9A=8C=EB=A7=81=ED=81=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20&=20=EB=A7=81=ED=81=AC=20=ED=81=B4=EB=A6=AD?= =?UTF-8?q?=EC=8B=9C=20APIM=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EB=A9=B4=20=EB=A1=9C=EA=B7=B8=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onl/transaction/tracking/trackingMan.jsp | 24 ++- .../eai/rms/common/login/MainController.java | 77 +++++++++ .../eai/rms/common/util/CommonConstants.java | 4 + .../entity/onl/logger/EAILogRepository.java | 2 + .../onl/logger/EAILogRepositoryImpl.java | 31 ++++ .../data/entity/onl/logger/EAILogService.java | 2 + .../entity/onl/logger/EAILogServiceImpl.java | 7 + .../djb/apistatus/ApiStatusRepository.java | 19 +- .../ext/djb/apistatus/ApiStatusService.java | 162 ++++++++++++++++-- .../rms/ext/djb/ums/service/UmsService.java | 19 +- .../eai/rms/ext/djb/ums/vo/SwingDataVO.java | 5 + .../tracking/DbTrackingController.java | 18 +- 12 files changed, 334 insertions(+), 36 deletions(-) diff --git a/WebContent/jsp/onl/transaction/tracking/trackingMan.jsp b/WebContent/jsp/onl/transaction/tracking/trackingMan.jsp index 6b09874..a9d0880 100644 --- a/WebContent/jsp/onl/transaction/tracking/trackingMan.jsp +++ b/WebContent/jsp/onl/transaction/tracking/trackingMan.jsp @@ -20,6 +20,11 @@ var url_view = ''; var url_excel = ''; var GUID_LENGTH = <%=DbTrackingService.GUID_LENGTH%>; + // 로그인 딥링크(세션) 또는 화면 URL에 검색조건 파라미터가 직접 붙어 들어온 경우, 최초 진입시 자동조회 + // 세션 딥링크는 파라미터명 그대로 모델에 등록되어 있으므로(예: ${searchStartYYYYMMDD}) bare 변수로 확인한다. + var isDeepLinkSearch = ${not empty searchStartYYYYMMDD + or not empty param.searchStartYYYYMMDD + or not empty param.searchEndYYYYMMDD}; var isProdServer = <%=com.eactive.eai.common.util.SystemUtil.isProdServer()%>; var roleString = "<%=SessionManager.getRoleIdString(request)%>"; let totalLogs = 0; @@ -441,6 +446,10 @@ getTransactionCounts(); }); + if (isDeepLinkSearch) { + $("#btn_search").click(); + } + $("input[name^=search]").keydown(function (key) { if (key.keyCode == 13) { $("#btn_search").click(); @@ -568,7 +577,7 @@
- +    @@ -595,11 +604,11 @@ <%=localeMessage.getString("tracking.msgrecvtm")%> - - + + ~ - - + + @@ -617,8 +626,9 @@ <%=localeMessage.getString("tracking.logtype")%> - - + + + 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 bf02e01..83bd372 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 @@ -6,6 +6,9 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; @@ -64,6 +67,18 @@ public class MainController implements InterceptorSkipController { private static final String REDIRECT_LOGIN_URL = "redirect:/emergency.jsp"; + // 로그인시 target 파라미터로 지정 가능한 딥링크 대상화면 식별자 -> mainPage 경로 매핑 + // (신규 대상화면 추가시 이 맵에만 등록하면 됨) + private static final Map DEEPLINK_TARGET_MAIN_PAGE_MAP; + static { + Map map = new HashMap<>(); + map.put("tracking", "/onl/transaction/tracking/trackingMan.view"); // 로그조회 + DEEPLINK_TARGET_MAIN_PAGE_MAP = Collections.unmodifiableMap(map); + } + + // 로그인 완료 후 이동할 딥링크 대상화면 식별자를 담아두는 세션 Key명 + private static final String SESSION_PENDING_DEEPLINK_TARGET = "pendingDeepLinkTarget"; + private static final String USER_STATUS_LOCKED = "2"; private static final int DEFAULT_MAX_LOGIN_FAIL_COUNT = 5; private static final int DEFAULT_PASSWORD_HISTORY_COUNT = 5; @@ -121,9 +136,45 @@ public class MainController implements InterceptorSkipController { logger.info("비상 로그인 모드 활성화 - SSO/SMS 인증 우회"); } + savePendingDeepLink(request, session); + return "/common/screen/emergency"; } + /** + * 로그인 페이지에 target 파라미터(딥링크 대상화면 식별자)가 있으면, + * 로그인 성공 후 이동할 화면 정보와 그 화면에 전달할 나머지 파라미터를 세션에 임시 저장한다. + * target/fullScreen/emergency 를 제외한 나머지 파라미터는 그대로(이름 변경 없이) 전달되므로, + * 로그인 URL에는 대상화면이 실제 사용하는 검색조건 파라미터명(예: searchEaiSvcName, searchStartYYYYMMDD)을 + * 그대로 넘겨야 한다. (실제 사용은 각 대상화면의 컨트롤러가 담당) + */ + private void savePendingDeepLink(HttpServletRequest request, HttpSession session) { + String target = request.getParameter("target"); + + if (StringUtils.isBlank(target) || !DEEPLINK_TARGET_MAIN_PAGE_MAP.containsKey(target)) { + return; + } + + session.setAttribute(SESSION_PENDING_DEEPLINK_TARGET, target); + + Map deepLinkParams = new HashMap<>(); + Enumeration paramNames = request.getParameterNames(); + + while (paramNames.hasMoreElements()) { + String name = paramNames.nextElement(); + + if ("target".equals(name) || "fullScreen".equals(name) || "emergency".equals(name)) { + continue; + } + + deepLinkParams.put(name, request.getParameter(name)); + } + + session.setAttribute(CommonConstants.PENDING_DEEPLINK_PARAMS, deepLinkParams); + + logger.info("딥링크 로그인 요청 - target: " + target); + } + @RequestMapping("/login.do") public String preMain(HttpServletRequest request, LoginVo dto) { @@ -561,9 +612,35 @@ public class MainController implements InterceptorSkipController { if (!processServiceType(request, dto)) { return REDIRECT_LOGIN_URL; } session.setAttribute(CommonConstants.LOGIN, dto); + + String deepLinkMainPage = consumePendingDeepLinkMainPage(request, session); + if (deepLinkMainPage != null) { + return "redirect:/main.do?mainPage=" + deepLinkMainPage; + } + return "redirect:/main.do"; } + // 로그인 시 저장해둔 딥링크 대상화면(target)을 1회성으로 꺼내 mainPage 경로로 변환한다. + // mainPage 값은 iframe src로 그대로 쓰이는 절대경로이므로 contextPath 를 반드시 포함해야 한다. + // (검색조건 등 나머지 파라미터는 CommonConstants.PENDING_DEEPLINK_PARAMS 에 남겨두고, + // 대상화면의 컨트롤러가 화면 진입 시점에 직접 소비한다) + private String consumePendingDeepLinkMainPage(HttpServletRequest request, HttpSession session) { + String target = (String) session.getAttribute(SESSION_PENDING_DEEPLINK_TARGET); + session.removeAttribute(SESSION_PENDING_DEEPLINK_TARGET); + + if (target == null) { + return null; + } + + String mainPage = DEEPLINK_TARGET_MAIN_PAGE_MAP.get(target); + if (mainPage == null) { + return null; + } + + return request.getContextPath() + mainPage; + } + // 사용자 최신 로그인 정보 업데이트 private void updateUserInfo(HttpServletRequest request, UserInfo userInfo) { String lastLoginIp = IpUtil.getClientIp(request); diff --git a/src/main/java/com/eactive/eai/rms/common/util/CommonConstants.java b/src/main/java/com/eactive/eai/rms/common/util/CommonConstants.java index ff3116a..dcd44c2 100644 --- a/src/main/java/com/eactive/eai/rms/common/util/CommonConstants.java +++ b/src/main/java/com/eactive/eai/rms/common/util/CommonConstants.java @@ -117,6 +117,10 @@ public class CommonConstants { public static final String loginId_KEY = "loginId"; public static final String LOGIN ="login"; + + // 로그인 딥링크(target 파라미터)로 넘어온 검색조건 등 부가 파라미터를 임시 보관하는 세션 Key명. + // 대상화면의 컨트롤러가 화면 진입 시 1회성으로 꺼내 쓰고 세션에서 제거해야 한다. + public static final String PENDING_DEEPLINK_PARAMS = "pendingDeepLinkParams"; public static final String USER_SERVICETYPE_NO_CHECK = "NO_CHECK"; diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepository.java index b89448c..5b7f849 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepository.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepository.java @@ -19,4 +19,6 @@ public interface EAILogRepository { List getApiLog(Class class1, String apiServiceNumber); + String findFirstErrorContent(Class clazz, String apiId, String rangeStart, String rangeEnd); + } diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java index 6111f94..78e103c 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogRepositoryImpl.java @@ -261,4 +261,35 @@ public class EAILogRepositoryImpl implements EAILogRepository { .fetch(); } + /** + * 지정 구간에서 apiId의 장애(시스템 오류) 로그 중 가장 먼저 발생한 1건의 오류내용(EAIERRCTNT). + * + *

장애 판단 기준은 {@link #appendConditions}의 {@code searchErrorYn=Y} 분기와 동일하다 - + * EAIERRCD에 값이 있고, 업무 오류(ERRCODE_BIZ)가 아닌 경우만 장애로 본다.

+ */ + @Override + public String findFirstErrorContent(Class clazz, String apiId, String rangeStart, String rangeEnd) { + QEAILog qeaiLog = QEAILog.eAILog; + EntityPathBase entityPathBase = new EntityPathBase<>(clazz, qeaiLog.getMetadata().getName()); + + JPAQuery query = new JPAQueryFactory(em) + .select(qeaiLog.eaierrctnt) + .from(entityPathBase) + .where(qeaiLog.eaisvcname.eq(apiId)) + .where(qeaiLog.id.msgdpstyms.between(rangeStart, rangeEnd)) + .where(qeaiLog.eaierrcd.trim().isNotEmpty()); + + List bizErrCodes = codeService.findByIdCodegroup(ERRCODE_BIZ_GROUP).stream() + .map(code -> code.getId().getCode()) + .collect(Collectors.toList()); + + if (!bizErrCodes.isEmpty()) { + query.where(qeaiLog.eaierrcd.notIn(bizErrCodes)); + } + + return query.orderBy(qeaiLog.id.msgdpstyms.asc(), qeaiLog.id.eaisvcserno.asc(), qeaiLog.id.logprcssserno.asc()) + .limit(1) + .fetchOne(); + } + } diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogService.java index 215687b..e8faf8d 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogService.java @@ -21,4 +21,6 @@ public interface EAILogService { Map getTransactionCounts(EAILogSearch eaiLogSearch); ApiLogDTO getApiLog(String txDate, String apiServiceNumber); + + String findFirstErrorContent(String apiId, String rangeStart, String rangeEnd); } diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogServiceImpl.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogServiceImpl.java index d27e031..c4c27e9 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogServiceImpl.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/logger/EAILogServiceImpl.java @@ -148,4 +148,11 @@ public class EAILogServiceImpl implements EAILogService { return apiLogDTO; } + @Override + public String findFirstErrorContent(String apiId, String rangeStart, String rangeEnd) { + long time = CommonUtil.getCalendar(rangeStart).getTimeInMillis(); + EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time); + return eaiLogRepository.findFirstErrorContent(eaiLog.getClass(), apiId, rangeStart, rangeEnd); + } + } 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 1aa9e1f..58e6452 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 @@ -46,12 +46,10 @@ public interface ApiStatusRepository extends BaseRepository { + " FROM (" + " SELECT A.EAISVCNAME" + " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE" - // 점검(KIND='MAINTENANCE')만 CTRL_YN 대상이다. KIND 를 안 걸면 관리자가 시작~종료 시각을 - // 넣어 등록한 장애 공지까지 '점검중'으로 오판해 CHECK_START 가 발생한다. + " , 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 Y.KIND = 'MAINTENANCE'" - + " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) " + + " AND :rangeEnd 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'" @@ -61,8 +59,8 @@ public interface ApiStatusRepository extends BaseRepository { + " FROM API_STATS_MINUTE" + " WHERE A.EAISVCNAME = API_NAME" + " AND TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')" - + " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:errorRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')" - + " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS ERR_YN" + + " BETWEEN TO_CHAR(:errorRangeStart,'YYYYMMDDHH24MI')" + + " AND TO_CHAR(:rangeEnd,'YYYYMMDDHH24MI'))) AS ERR_YN" + " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'" + " WHEN RESP_SUM / TOTAL >= :delayAvgRespTime THEN 'Y'" + " ELSE 'N' END" @@ -72,16 +70,17 @@ public interface ApiStatusRepository extends BaseRepository { + " FROM API_STATS_MINUTE" + " WHERE A.EAISVCNAME = API_NAME" + " AND TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')" - + " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:delayRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')" - + " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS DELAY_YN" + + " BETWEEN TO_CHAR(:delayRangeStart,'YYYYMMDDHH24MI')" + + " AND TO_CHAR(:rangeEnd,'YYYYMMDDHH24MI'))) AS DELAY_YN" + " FROM TSEAIHE01 A LEFT OUTER JOIN API_STATUS B ON A.EAISVCNAME = B.EAISVCNAME" + " )" + " ) A WHERE EVENT != 'STAY'") List findApiStatusEvents( @Param("errorRate") int errorRate, - @Param("errorRangeMinute") int errorRangeMinute, - @Param("delayRangeMinute") int delayRangeMinute, - @Param("delayAvgRespTime") int delayAvgRespTime + @Param("delayAvgRespTime") int delayAvgRespTime, + @Param("errorRangeStart") LocalDateTime errorRangeStart, + @Param("delayRangeStart") LocalDateTime delayRangeStart, + @Param("rangeEnd") LocalDateTime rangeEnd ); /** diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java index 3ae8864..ad8f8d3 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiStatusService.java @@ -2,7 +2,10 @@ package com.eactive.eai.rms.ext.djb.apistatus; import java.math.BigDecimal; import java.math.RoundingMode; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -10,6 +13,7 @@ import java.util.Map; import javax.annotation.PostConstruct; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +30,7 @@ import com.eactive.eai.rms.common.context.MonitoringContext; import com.eactive.eai.rms.data.entity.man.role.Role; import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiErrorRateRow; import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent; +import com.eactive.eai.rms.data.entity.onl.logger.EAILogService; import com.eactive.eai.rms.ext.djb.ums.UmsManager; @@ -47,6 +52,9 @@ public class ApiStatusService { @Autowired private MonitoringContext monitoringContext; + @Autowired + private EAILogService eaiLogService; + @Autowired @Qualifier("transactionManagerForEMS") private PlatformTransactionManager transactionManager; @@ -70,11 +78,20 @@ public class ApiStatusService { logErrorRates(errorRate, errorRangeMinute, delayRangeMinute, delayAvgRespTime); + // 조회구간을 WAS 시각 기준으로 계산해 쿼리에 직접 바인딩한다. + // DB의 SYSDATE 를 기준으로 하면 DB 서버와 WAS 서버의 시각이 어긋날 때 판정구간이 + // 미묘하게 달라질 수 있고, 알림 메시지에 넣는 로그조회 딥링크의 구간과도 어긋나게 된다. + // 장애/지연 판정기준(분)이 서로 다를 수 있으므로 각자의 시작시간을 따로 계산한다. + LocalDateTime rangeEnd = LocalDateTime.now(); + LocalDateTime errorRangeStart = rangeEnd.minusMinutes(errorRangeMinute); + LocalDateTime delayRangeStart = rangeEnd.minusMinutes(delayRangeMinute); + List list = apiStatusRepository.findApiStatusEvents( errorRate, - errorRangeMinute, - delayRangeMinute, - delayAvgRespTime + delayAvgRespTime, + errorRangeStart, + delayRangeStart, + rangeEnd ); HashMap> eventMap = new HashMap<>(); @@ -112,7 +129,7 @@ public class ApiStatusService { //내부직원 스윙챗 발송 if (monitoringContext.getBooleanProperty(MonitoringContext.DJB_UMS_MESSENGER_APIMONITOR_ENABLED, true)) { - HashMap params = getSwingChatMessage(event, apiIds); + HashMap params = getSwingChatMessage(event, apiIds, errorRangeStart, delayRangeStart, rangeEnd); ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, params); } @@ -247,41 +264,152 @@ public class ApiStatusService { } - private HashMap getSwingChatMessage(String event, List apiIds) { + private HashMap getSwingChatMessage(String event, List apiIds, + LocalDateTime errorRangeStart, LocalDateTime delayRangeStart, LocalDateTime rangeEnd) { String message = ""; switch (event) { - case "CHECK_START": - message = "API 서비스 점검이 시작되었습니다"; //점검 + case "CHECK_START": + message = "API 서비스 점검이 시작되었습니다"; //점검 break; - case "CHECK_END": + case "CHECK_END": message = "API 서비스 점검이 완료되었습니다"; //정상 break; - case "ERROR_START": + case "ERROR_START": message = "API 서비스 장애가 발생하였습니다"; //장애 break; - case "ERROR_END": + case "ERROR_END": message = "API 서비스 장애가 복구되었습니다"; //정상 break; - case "DELAY_START": + case "DELAY_START": message = "API 서비스 지연이 발생하였습니다"; //지연 break; - case "DELAY_END": + case "DELAY_END": message = "API 서비스 지연이 복구되었습니다"; //정상 break; default: message = ""; } - - if (apiIds.size() > 1) { - message += "\n" + apiIds.get(0) + "외 " + (apiIds.size()-1) + "종"; + + if ("CHECK_START".equals(event) || "CHECK_END".equals(event)) { + if (apiIds.size() > 1) { + message += "
- " + apiIds.get(0) + "외 " + (apiIds.size()-1) + "종"; + } else { + message += "
- " + apiIds.get(0); + } } else { - message += "\n- " + apiIds.get(0); + message += "
- 인터페이스 : " + String.join(", ", apiIds); } - + + // 장애 발생시에만 첫번째 API ID의 장애 로그(가장 먼저 발생한 1건) 오류내용을 메시지에 붙인다. + if ("ERROR_START".equals(event) && !apiIds.isEmpty()) { + String errorContent = findFirstErrorContent(apiIds.get(0), errorRangeStart, rangeEnd); + if (StringUtils.isNotBlank(errorContent)) { + message += "
- 에러메세지 : " + errorContent; + } + } + + // 장애/지연 발생시에만 첫번째 API ID로 로그조회 화면 바로가기 링크를 메시지 끝에 추가한다. + // (장애/지연은 판정기준 시간간격이 서로 다를 수 있어 각자의 판정구간을 사용한다) + // UmsService.makeBodyData() 가 이 마커 뒤의 내용을 URL로 보고 본문에서 떼어내 확인URL(PC) 필드로 옮긴다. + LocalDateTime linkRangeStart = "ERROR_START".equals(event) ? errorRangeStart + : "DELAY_START".equals(event) ? delayRangeStart + : null; + + if (linkRangeStart != null) { + String loginUrl = buildTrackingLoginUrl(linkRangeStart, rangeEnd); + if (loginUrl != null) { + message += "
- 로그조회링크 : " + loginUrl; + } + } + HashMap params = new HashMap<>(); params.put("message", message); + return params; } + + private static final DateTimeFormatter DEEPLINK_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final DateTimeFormatter DEEPLINK_TIME_FORMATTER = DateTimeFormatter.ofPattern("HHmmss"); + + // MSGDPSTYMS(로그 테이블 PK 일부) 형식과 동일 - yyyyMMddHHmmss + 밀리초(3자리) + private static final DateTimeFormatter LOG_RANGE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + // WAR는 항상 이 context path로 배포된다 (readme.md/build.gradle 기준) + private static final String ADMIN_CONTEXT_PATH = "/monitoring"; + + /** + * 판정 구간에서 apiId의 장애 로그 중 가장 먼저 발생한 1건의 오류내용(EAIERRCTNT). + * + *

장애 판단 기준은 {@code EAILogRepositoryImpl}의 에러로그 검색 조건과 동일 - EAIERRCD에 값이 있고 + * 업무 오류(공통코드 ERRCODE_BIZ)가 아닌 경우만 장애로 본다.

+ * + *

알림 발송 자체를 막아서는 안 되므로 조회 실패는 로그만 남기고 삼킨다.

+ */ + private String findFirstErrorContent(String apiId, LocalDateTime rangeStart, LocalDateTime rangeEnd) { + try { + String start = rangeStart.format(LOG_RANGE_FORMATTER) + "000"; + String end = rangeEnd.format(LOG_RANGE_FORMATTER) + "999"; + return eaiLogService.findFirstErrorContent(apiId, start, end); + } catch (Exception e) { + log.warn("장애 로그 조회 실패 - {}", apiId, e); + return null; + } + } + + /** + * 관리콘솔 로그인 화면으로 이동해 로그인 완료 즉시 로그조회 화면에서 + * 해당 시간구간의 에러로그로 자동조회 되도록 하는 딥링크 URL을 만든다. + * (로그조회 화면 파라미터명과 동일해야 하며, {@code MainController.loginForm.do} 의 + * target=tracking 딥링크 처리와 짝을 이룬다) + * + *

API ID는 여러 건일 수 있어 검색조건으로 넣지 않고, 대신 장애 로그(searchErrorYn=Y)로만 좁힌다.

+ * + * @return 현재 서버 IP에 대한 WEBSERVERPORT 프로퍼티를 못 찾으면 null + */ + private String buildTrackingLoginUrl(LocalDateTime rangeStart, LocalDateTime rangeEnd) { + String baseUrl = resolveCurrentServerBaseUrl(); + if (baseUrl == null) { + return null; + } + + StringBuilder url = new StringBuilder(baseUrl); + url.append("/loginForm.do?target=tracking"); + url.append("&searchErrorYn=Y"); + url.append("&searchStartYYYYMMDD=").append(rangeStart.format(DEEPLINK_DATE_FORMATTER)); + url.append("&searchStartHHMM=").append(rangeStart.format(DEEPLINK_TIME_FORMATTER)); + url.append("&searchEndYYYYMMDD=").append(rangeEnd.format(DEEPLINK_DATE_FORMATTER)); + url.append("&searchEndHHMM=").append(rangeEnd.format(DEEPLINK_TIME_FORMATTER)); + + return url.toString(); + } + + /** + * 현재 서버(이 WAS가 뜬 인스턴스)의 IP로, 모니터링 프로퍼티 그룹명이 + * {@code Monitoring{}} 형태로 등록된 WEBSERVERPORT 프로퍼티 값을 찾아 base URL을 만든다. + * ({@link MonitoringContext#getPortMapper()}가 이미 그룹명에서 IP를 뽑아 IP -> 포트로 + * 매핑해두므로 그대로 재사용한다. {@code EaiServerInfoServiceImpl}과 동일한 조회 방식) + * + * @return WEBSERVERPORT 프로퍼티가 없으면 null + */ + private String resolveCurrentServerBaseUrl() { + String ip; + try { + ip = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.warn("현재 서버 IP 조회 실패 - 로그조회 딥링크 생략", e); + return null; + } + + Map portMapper = monitoringContext.getPortMapper(); + String port = (portMapper != null) ? portMapper.get(ip) : null; + + if (StringUtils.isEmpty(port)) { + log.warn("WEBSERVERPORT 프로퍼티 없음 (Monitoring{" + ip + "}) - 로그조회 딥링크 생략"); + return null; + } + + return "http://" + ip + ":" + port + ADMIN_CONTEXT_PATH; + } } diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/ums/service/UmsService.java b/src/main/java/com/eactive/eai/rms/ext/djb/ums/service/UmsService.java index 65c17ca..1f2fdbf 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/ums/service/UmsService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/ums/service/UmsService.java @@ -48,6 +48,9 @@ public class UmsService { private static final String BZWK_CD = "EBK"; //업무코드 private static final String SUB_BZWK_CD = "EBG"; //서브업무코드 + // message 끝에 붙는 확인URL(PC) 마커. 이 뒤의 내용은 URL로 간주해 본문에서 떼어낸다. + private static final String CHECK_URL_FOR_PC_MARKER = "
- 로그조회링크 : "; + private final MonitoringPropertyService propertyService; private final MessageRequestService messageRequestService; @@ -256,7 +259,21 @@ public class UmsService { SwingDataVO swiDataVO = new SwingDataVO(); swiDataVO.setNotiCode("CNO_FEP_API_0002"); //0001:긴급, 0002:중요 swiDataVO.setNotiTitle("알림"); - swiDataVO.setNotiContent(messageRequest.getMessage()); + + // message 끝에 "로그조회링크 : URL" 이 붙어 있으면(ApiStatusService 참고) 본문에서는 + // 떼어내고 URL만 뽑아 확인URL(PC) 필드에 따로 담는다. + String notiContent = messageRequest.getMessage(); + String checkUrlForPc = ""; + + int checkUrlForPcIdx = notiContent.indexOf(CHECK_URL_FOR_PC_MARKER); + if (checkUrlForPcIdx >= 0) { + checkUrlForPc = notiContent.substring(checkUrlForPcIdx + CHECK_URL_FOR_PC_MARKER.length()).trim(); + notiContent = notiContent.substring(0, checkUrlForPcIdx).trim(); + } + + swiDataVO.setNotiContent(notiContent); + swiDataVO.setCheckUrlForPc(checkUrlForPc); + // 스윙챗 알림 데이터 swiDataVO.setReceiverInfo(getSwingReceiver(messageRequest)); diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/ums/vo/SwingDataVO.java b/src/main/java/com/eactive/eai/rms/ext/djb/ums/vo/SwingDataVO.java index 60bd2b6..16f6b10 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/ums/vo/SwingDataVO.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/ums/vo/SwingDataVO.java @@ -15,7 +15,12 @@ public class SwingDataVO { /** 알림내용 */ private String notiContent; + + /** 확인URL(PC) */ + private String checkUrlForPc; /** 수신자정보 */ private List receiverInfo; + + } diff --git a/src/main/java/com/eactive/eai/rms/onl/transaction/tracking/DbTrackingController.java b/src/main/java/com/eactive/eai/rms/onl/transaction/tracking/DbTrackingController.java index feb724c..14e2c2a 100644 --- a/src/main/java/com/eactive/eai/rms/onl/transaction/tracking/DbTrackingController.java +++ b/src/main/java/com/eactive/eai/rms/onl/transaction/tracking/DbTrackingController.java @@ -8,6 +8,7 @@ import com.eactive.eai.data.entity.onl.server.EAIServer; import com.eactive.eai.rms.common.combo.ComboVo; import com.eactive.eai.rms.common.context.MonitoringContext; import com.eactive.eai.rms.common.logging.UserAccessLogger; +import com.eactive.eai.rms.common.util.CommonConstants; import com.eactive.eai.rms.common.util.CommonUtil; import com.eactive.eai.rms.common.vo.GridResponse; import com.eactive.eai.rms.data.entity.onl.adapter.AdapterGroupService; @@ -42,9 +43,11 @@ import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.ModelAndView; +import org.springframework.ui.ModelMap; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import java.io.ByteArrayOutputStream; import java.util.*; @@ -73,7 +76,20 @@ public class DbTrackingController { } @GetMapping(value = "/onl/transaction/tracking/trackingMan.view") - public String viewList() { + public String viewList(HttpServletRequest request, ModelMap model) { + HttpSession session = request.getSession(); + + // 로그인 딥링크(target=tracking)로 넘어온 검색조건 파라미터를 1회성으로 소비한다. + // 파라미터명은 이 화면의 검색조건 input name(searchEaiSvcName, searchStartYYYYMMDD 등)과 동일해야 하며, + // 그 이름 그대로 모델에 등록해 JSP에서 파라미터명 그대로(${searchXXX}) 꺼내 쓸 수 있게 한다. + @SuppressWarnings("unchecked") + Map pendingParams = (Map) session.getAttribute(CommonConstants.PENDING_DEEPLINK_PARAMS); + + if (pendingParams != null) { + session.removeAttribute(CommonConstants.PENDING_DEEPLINK_PARAMS); + model.addAllAttributes(pendingParams); + } + return "/onl/transaction/tracking/trackingMan"; }