api 상태 변화 감지 & 웹훅 발송

This commit is contained in:
eastargh
2026-06-01 17:37:06 +09:00
parent 737b493f7c
commit 0d7d006f92
33 changed files with 505 additions and 277 deletions
@@ -69,6 +69,7 @@
<context:component-scan base-package="com.eactive.apim.portal" />
<context:component-scan base-package="com.eactive.ext.kjb.statistics" />
<context:component-scan base-package="com.eactive.ext.djb" />
<bean id="cacheManager"
@@ -107,6 +107,17 @@ public class UserInfoService extends
}).collect(Collectors.toList());
}
public List<UserInfo> findAllByRoleId(String roleId) {
QUserInfo qUserInfo = QUserInfo.userInfo;
QUserRole qUserRole = QUserRole.userRole;
return getJPAQueryFactory()
.selectFrom(qUserInfo)
.join(qUserRole).on(qUserInfo.userid.eq(qUserRole.id.userId))
.where(qUserRole.id.roleId.eq(roleId))
.fetch();
}
public Page<UserInfo> findByUsername(Pageable pageable, String username) {
QUserInfo qUserInfo = QUserInfo.userInfo;
BooleanExpression predicate = qUserInfo.isNotNull();
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
import java.time.LocalDateTime;
@@ -1,7 +1,8 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
public interface ApiStatusEvent {
String getEaisvcname();
String getEaisvcdesc();
String getPrevStatusCode();
String getEvent();
}
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
public interface InflowTokenInsufficient {
String getEaisvcname();
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable;
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable;
import javax.persistence.Column;
@@ -0,0 +1,33 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "PTL_WEBHOOK_REQ")
@Getter
@NoArgsConstructor
public class WebhookReq {
@Id
@Column(name = "ID", length = 36, nullable = false)
private String id;
@Column(name = "ORG_ID", length = 100)
@Comment("법인 ID")
private String orgId;
@Column(name = "TARGET_URL", length = 500, nullable = false)
@Comment("웹훅 수신 URL")
private String targetUrl;
@Column(name = "SECRET", length = 200)
@Comment("HMAC 서명용 Secret Key")
private String secret;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_API")
@Getter
@NoArgsConstructor
public class WebhookReqApi implements Serializable {
@EmbeddedId
private WebhookReqApiId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqApiId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "API_ID", nullable = false, length = 100)
@Comment("API ID")
private String apiId;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_EVENT")
@Getter
@NoArgsConstructor
public class WebhookReqEvent implements Serializable {
@EmbeddedId
private WebhookReqEventId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqEventId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "EVENT_TYPE", nullable = false, length = 50)
@Comment("이벤트 유형 (CONTROL_START, ERROR_START 등)")
private String eventType;
}
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.webhook;
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -1,69 +0,0 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
public void updateApiStatus(HashMap<String, String> param) {
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
Integer.parseInt(param.get("errorRate")),
Integer.parseInt(param.get("errorRangeMinute")),
Integer.parseInt(param.get("delayRangeMinute")),
Integer.parseInt(param.get("delayAvgRespTime"))
);
for (ApiStatusEvent event : list) {
String newStatusCode = resolveStatusCode(event.getEvent());
if (newStatusCode == null) continue;
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
eventPublisher.publishEvent(ApiStatusChangedEvent.from(event)); // 트랜잭션 커밋 후 리스너 실행
log.debug("이벤트 전파");
}
}
private String resolveStatusCode(String event) {
switch (event) {
case "CONTROL_START": return "C"; //점검
case "CONTROL_END": return "N"; //정상
case "ERROR_START": return "E"; //장애
case "ERROR_END": return "N"; //정상
case "DELAY_START": return "D"; //지연
case "DELAY_END": return "N"; //정상
default:
log.warn("알 수 없는 이벤트: {}", event);
return null;
}
}
}
@@ -1,10 +1,12 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
@@ -22,9 +24,11 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
@Query(nativeQuery = true, value =
" SELECT EAISVCNAME"
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
+ " , PREV_STATUS_CODE "
+ " , EVENT"
+ " FROM ("
+ " SELECT EAISVCNAME"
+ " , STATUS_CODE AS PREV_STATUS_CODE "
+ " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CONTROL_START'"
+ " WHEN STATUS_CODE = 'C' AND CTRL_YN = 'N' THEN 'CONTROL_END'"
+ " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'"
@@ -34,14 +38,14 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " ELSE 'STAY' END AS EVENT"
+ " FROM ("
+ " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, '1') AS STATUS_CODE"
+ " , NVL((SELECT 'Y' FROM TSEAITI01"
+ " WHERE (TO_CHAR(SYSDATE,'HH24MI') BETWEEN SUBSTR(EAICTRLDSTICCTNT,16,4) AND SUBSTR(EAICTRLDSTICCTNT,21,4)"
+ " OR SUBSTR(EAICTRLDSTICCTNT,16,9) = '0000|0000')"
+ " AND A.EAISVCNAME = EAICTRLNAME),'N') AS CTRL_YN"
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
+ " , NVL(( SELECT 'Y' FROM EMSAPP.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 "
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END"
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
+ " FROM API_STATS_MINUTE"
@@ -0,0 +1,141 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
import com.eactive.ext.djb.apistatus.DjbIncidentDetectionService;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private UmsManager ums;
@Autowired
private DjbIncidentDetectionService djbService;
public void updateApiStatus(HashMap<String, String> param) {
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
Integer.parseInt(param.get("errorRate")),
Integer.parseInt(param.get("errorRangeMinute")),
Integer.parseInt(param.get("delayRangeMinute")),
Integer.parseInt(param.get("delayAvgRespTime"))
);
HashMap<String, List<String>> eventMap = new HashMap<>();
for (ApiStatusEvent event : list) {
String newStatusCode = resolveStatusCode(event.getEvent());
if (newStatusCode == null) continue;
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
//이벤트별로 api분리 저장
List<String> apis = (List<String>)eventMap.get(event.getEvent());
if (apis == null) {
apis = new ArrayList<String>();
apis.add(event.getEaisvcname());
eventMap.put(event.getEvent(), apis);
} else {
apis.add(event.getEaisvcname());
}
}
//이벤트별로 ums, webhook을 발송한다
for (Map.Entry<String, List<String>> entry : eventMap.entrySet()) {
String event = entry.getKey();
List<String> apiIds = entry.getValue();
String message = getSwingChatMessage(event, apiIds);
ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, message);
//제휴사 webhook 발송
ums.sendWebhook(event, apiIds);
//개발자포탈 API상태모니터링 화면을 위한 처리
djbService.detect(event, apiIds);
// if (event.endsWith("_END")) {
// djbService.recovered(event, apiIds);
// } else {
// djbService.detect(event, apiIds);
// }
}
}
private String resolveStatusCode(String event) {
switch (event) {
case "CONTROL_START": return "C"; //점검
case "CONTROL_END": return "N"; //정상
case "ERROR_START": return "E"; //장애
case "ERROR_END": return "N"; //정상
case "DELAY_START": return "D"; //지연
case "DELAY_END": return "N"; //정상
default:
log.warn("알 수 없는 이벤트: {}", event);
return null;
}
}
private String getSwingChatMessage(String event, List<String> apiIds) {
String message = "";
switch (event) {
case "CONTROL_START":
message = "API 서비스 점검이 시작되었습니다"; //점검
break;
case "CONTROL_END":
message = "API 서비스 점검이 완료되었습니다"; //정상
break;
case "ERROR_START":
message = "API 서비스 장애가 발생하였습니다"; //장애
break;
case "ERROR_END":
message = "API 서비스 장애가 복구되었습니다"; //정상
break;
case "DELAY_START":
message = "API 서비스 지연이 발생하였습니다"; //지연
break;
case "DELAY_END":
message = "API 서비스 지연이 복구되었습니다"; //정상
break;
default:
message = "";
}
return message + "\n- " + String.join(",", apiIds);
}
}
@@ -1,25 +0,0 @@
package com.eactive.eai.rms.ext.djb.async;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@@ -1,40 +0,0 @@
package com.eactive.eai.rms.ext.djb.async;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent;
import com.eactive.eai.rms.ext.djb.swing.service.SwingSendManager;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookSendManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@RequiredArgsConstructor
public class AsyncEventListener {
private final WebhookSendManager webhookSendManager;
private final SwingSendManager swingSendManager;
@Async("asyncExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onApiStatusChanged(ApiStatusChangedEvent event) {
log.debug("API 상태 변경 이벤트 수신: {} {}", event.getEaisvcname(), event.getEvent());
webhookSendManager.send(event.getEaisvcname(), event.getEvent());
swingSendManager.send(null, 0, 0);
}
@Async("asyncExecutor")
@EventListener // 트랜잭션 write 없이 read만 하므로 TransactionalEventListener 불필요
public void onInflowTokenFail(InflowTokenInsufficientEvent event) {
log.debug("유량제어 토큰획득 실패 이벤트 수신: {} {}건", event.getEaisvcname(), event.getCnt());
swingSendManager.send(event.getEaisvcname(), event.getCnt(), event.getRangeMinute());
}
}
@@ -1,18 +0,0 @@
package com.eactive.eai.rms.ext.djb.event;
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusEvent;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class ApiStatusChangedEvent {
private final String eaisvcname;
private final String eaisvcdesc;
private final String event;
public static ApiStatusChangedEvent from(ApiStatusEvent source) {
return new ApiStatusChangedEvent(source.getEaisvcname(), source.getEaisvcdesc(), source.getEvent());
}
}
@@ -1,6 +1,6 @@
package com.eactive.eai.rms.ext.djb.event;
package com.eactive.eai.rms.ext.djb.inflow;
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -1,9 +1,12 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLog;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLogId;
public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> {
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List;
@@ -9,7 +9,9 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
@Service
@@ -22,16 +24,15 @@ public class InflowTokenService {
private InflowTokenRepository inflowTokenRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
private UmsManager ums;
public void checkRecentFails(long rangeMinute) {
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
for (InflowTokenInsufficient info : rows) {
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
//내부직원 알림 발송
eventPublisher.publishEvent(InflowTokenInsufficientEvent.from(info, rangeMinute));
String message = "유량제어 토큰 획득 실패하였습니다 \n" + info.getEaisvcname();
ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, message);
}
}
}
@@ -17,7 +17,7 @@ import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusService;
import com.eactive.eai.rms.ext.djb.apistatus.ApiStatusService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
/**
@@ -88,11 +88,11 @@ public class ApiStatusMonitorJob implements Job {
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
ApiStatusService apiStatusUpdateService = appContext.getBean(ApiStatusService.class);
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
apiStatusUpdateService.updateApiStatus(param);
} catch (Exception e) {
log.error("ApiStatusUpdateJob execution failed", e);
throw new JobExecutionException(e);
@@ -18,7 +18,7 @@ import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenService;
import com.eactive.eai.rms.ext.djb.inflow.InflowTokenService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
/**
@@ -1,17 +0,0 @@
package com.eactive.eai.rms.ext.djb.swing.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class SwingSendManager {
public void send(String eaisvcname, long cnt, long rangeMinute) {
log.info("[Swing] 발송 준비: {} 최근 {}분 {}건", eaisvcname, rangeMinute, cnt);
// TODO: 알림 발송 로직
}
}
@@ -0,0 +1,71 @@
package com.eactive.eai.rms.ext.djb.util;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class UmsManager {
private static final Logger log = LoggerFactory.getLogger(UmsManager.class);
private final MessageHandlerService messageHandlerService;
private final UserInfoService userInfoService;
private final WebhookService webhookService;
/**
* 내부직원 메신저(Swing chat) 발송
* @param roleId
* @param messageCode
* @param message
* @return
*/
public void sendMessenger(String roleId, MessageCode messageCode, String message) {
List<UserInfo> users = userInfoService.findAllByRoleId(roleId);
if (users.isEmpty()) {
log.warn("sendMessenger: no users found for role '{}'", roleId);
return;
}
for (UserInfo user : users) {
MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid())
.username(user.getUsername())
.build();
HashMap<String, Object> params = new HashMap<>();
params.put("message", message);
messageHandlerService.publishEvent(messageCode, recipient, params);
}
}
/**
* event를 구둑중인 제휴사에게 웹훅 발송
* @param event
* @param apiIds
*/
public void sendWebhook(String event, List<String> apiIds) {
List<WebhookSendRequest> list = webhookService.findSendList(event, apiIds);
for (WebhookSendRequest req : list) {
webhookService.send(req);
}
}
}
@@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookReceiveService;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
@@ -38,16 +38,12 @@ public class WebhookSendController {
public ResponseEntity<Map<String, Object>> sendWebhook(
@RequestBody WebhookSendRequest request) {
WebhookSendLog result = webhookService.send(
request.getTargetUrl(),
request.getEventType(),
request.getData()
);
webhookService.send(request);
Map<String, Object> response = new HashMap<>();
response.put("logId", result.getId());
response.put("success", result.getSuccess());
response.put("statusCode", result.getStatusCode());
// response.put("logId", result.getId());
// response.put("success", result.getSuccess());
// response.put("statusCode", result.getStatusCode());
return ResponseEntity.ok(response);
}
@@ -6,8 +6,9 @@ import lombok.Setter;
@Getter
@Setter
public class WebhookSendRequest {
private String orgId;
private String targetUrl;
private String eventType;
private String secret;
private Object data;
}
@@ -0,0 +1,11 @@
package com.eactive.eai.rms.ext.djb.webhook.repository;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookReq;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
@EMSDataSource
public interface WebhookReqRepository extends JpaRepository<WebhookReq, String> {
}
@@ -10,7 +10,7 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
@Repository
@EMSDataSource
@@ -1,17 +0,0 @@
package com.eactive.eai.rms.ext.djb.webhook.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class WebhookSendManager {
public void send(String eaisvcname, String eventType) {
log.info("[Webhook] 발송 준비: {} {}", eaisvcname, eventType);
// TODO: PTL_WEBHOOK_REQ 조인 조회 후 WebhookService.send() 호출
}
}
@@ -2,12 +2,16 @@ package com.eactive.eai.rms.ext.djb.webhook.service;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -18,10 +22,16 @@ import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqApi;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqEvent;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookPayload;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -35,17 +45,58 @@ public class WebhookService {
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate;
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf";
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
private EntityManager entityManager;
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final int MAX_RETRY = 3;
@SuppressWarnings("unchecked")
public List<WebhookSendRequest> findSendList(String event, List<String> apiIds) {
QWebhookReq req = QWebhookReq.webhookReq;
QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent;
QWebhookReqApi api = QWebhookReqApi.webhookReqApi;
List<Tuple> tuples = new JPAQueryFactory(entityManager)
.select(req.orgId, req.targetUrl, req.secret, api.id.apiId)
.from(req)
.join(evt).on(req.id.eq(evt.id.webhookReqId))
.join(api).on(req.id.eq(api.id.webhookReqId))
.where(evt.id.eventType.eq(event)
.and(api.id.apiId.in(apiIds)))
.fetch();
// orgId + targetUrl + secret 기준으로 그룹핑, apiId를 data(List)로 집계
Map<String, WebhookSendRequest> resultMap = new LinkedHashMap<>();
for (Tuple t : tuples) {
String key = t.get(req.orgId) + "|" + t.get(req.targetUrl);
resultMap.computeIfAbsent(key, k -> {
WebhookSendRequest wr = new WebhookSendRequest();
wr.setOrgId(t.get(req.orgId));
wr.setTargetUrl(t.get(req.targetUrl));
wr.setSecret(t.get(req.secret));
wr.setEventType(event);
wr.setData(new ArrayList<String>());
return wr;
});
((List<String>) resultMap.get(key).getData()).add(t.get(api.id.apiId));
}
return new ArrayList<>(resultMap.values());
}
/**
* 웹훅 발송 메인 메서드
*/
@Transactional
public WebhookSendLog send(String targetUrl, String eventType, Object data) {
public void send(WebhookSendRequest req) {
String targetUrl = req.getTargetUrl();
String secret = req.getSecret();
String eventType = req.getEventType();
Object data = req.getData();
WebhookSendLog sendLog = new WebhookSendLog();
sendLog.setTargetUrl(targetUrl);
sendLog.setEventType(eventType);
@@ -57,7 +108,7 @@ public class WebhookService {
sendLog.setPayload(payloadJson);
// 2. Secret Key로 HMAC-SHA256 서명 생성
String signature = generateSignature(payloadJson);
String signature = generateSignature(secret, payloadJson);
sendLog.setSignature(signature);
// 3. HTTP 요청 헤더 구성
@@ -95,12 +146,13 @@ public class WebhookService {
eventType, targetUrl, e.getMessage());
}
return sendLogRepository.save(sendLog);
sendLogRepository.save(sendLog);
}
/**
* 실패 건 재시도
*/
@Transactional
public void retryFailedWebhooks() {
List<WebhookSendLog> failedLogs =
@@ -119,16 +171,16 @@ public class WebhookService {
log.error("[Webhook] 재시도 실패 - id: {}", failedLog.getId(), e);
}
}
}
}*/
/**
* HMAC-SHA256 서명 생성
* Java 17 미만은 HexFormat 미지원이므로 직접 변환
*/
private String generateSignature(String payload) throws Exception {
private String generateSignature(String secret, String payload) throws Exception {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(keySpec);
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
@@ -68,14 +68,14 @@ public class UmsDispatchService {
log.info("UmsDispatchService initialized - KjbUmsService and KjbEmailSendModule ready via singleton");
}
// private KjbUmsService getUmsService() {
// return KjbUmsService.getInstance();
// }
//
// private KjbEmailSendModule getEmailModule() {
// return KjbEmailSendModule.getInstance();
// }
//
private KjbUmsService getUmsService() {
return KjbUmsService.getInstance();
}
private KjbEmailSendModule getEmailModule() {
return KjbEmailSendModule.getInstance();
}
@SuppressWarnings("unchecked")
@Transactional(transactionManager = "transactionManagerForEMS")
public void processMessages() {
@@ -112,7 +112,7 @@ public class UmsDispatchService {
log.info("Starting to submit {} messages for processing", messagesToProcess.size());
for (MessageRequest message : messagesToProcess) {
//submitMessageTask(message);
submitMessageTask(message);
}
} else {
log.info("No pending messages found");
@@ -123,40 +123,40 @@ public class UmsDispatchService {
}
// private void submitMessageTask(MessageRequest message) {
// int queueSize = executorService.getQueue().size();
// log.debug("Current queue size before submission: {}", queueSize);
//
// executorService.submit(() -> {
// try {
// transactionTemplate.execute(status -> {
// try {
// // 광주은행에서는 사용하지 않는 메소드
//// processMessage(message);
//
// if (KjbUmsService.isAllowChannel(message)) {
// getUmsService().send(message);
// }
//
// if (KjbEmailSendModule.isAllowChannel(message)) {
// getEmailModule().sendEmail(message);
// }
//
// return null;
// } catch (Exception e) {
// log.error("Failed to process message: {} - Error: {}",
// message.getId(), e.getMessage(), e);
// updateMessageStatus(message, "FAILED");
// status.setRollbackOnly();
// return null;
// }
// });
// } catch (Exception e) {
// log.error("Transaction execution failed for message: {} - Error: {}",
// message.getId(), e.getMessage(), e);
// }
// });
// }
private void submitMessageTask(MessageRequest message) {
int queueSize = executorService.getQueue().size();
log.debug("Current queue size before submission: {}", queueSize);
executorService.submit(() -> {
try {
transactionTemplate.execute(status -> {
try {
// 광주은행에서는 사용하지 않는 메소드
// processMessage(message);
if (KjbUmsService.isAllowChannel(message)) {
getUmsService().send(message);
}
if (KjbEmailSendModule.isAllowChannel(message)) {
getEmailModule().sendEmail(message);
}
return null;
} catch (Exception e) {
log.error("Failed to process message: {} - Error: {}",
message.getId(), e.getMessage(), e);
updateMessageStatus(message, "FAILED");
status.setRollbackOnly();
return null;
}
});
} catch (Exception e) {
log.error("Transaction execution failed for message: {} - Error: {}",
message.getId(), e.getMessage(), e);
}
});
}
/**
* kbank 방식으로 광주은행에서는 사용하지 않음