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 @@
-
+
Auto Refresh
table_view <%= localeMessage.getString("button.excel") %>
@@ -595,11 +604,11 @@
<%=localeMessage.getString("tracking.msgrecvtm")%>
-
-
+
+
~
-
-
+
+
@@ -617,8 +626,9 @@
<%=localeMessage.getString("tracking.logtype")%>
- <%= localeMessage.getString("tracking.allLog") %>
- <%= localeMessage.getString("tracking.errLog") %>
+
+ <%= localeMessage.getString("tracking.allLog") %>
+ <%= localeMessage.getString("tracking.errLog") %>
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 extends EAILog> class1, String apiServiceNumber);
+ String findFirstErrorContent(Class extends EAILog> 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 extends EAILog> 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";
}