@@ -0,0 +1,240 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.url-cell { display:inline-block; max-width:380px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:middle; }
|
||||
.succ-y { color:#1a73e8; font-weight:bold; }
|
||||
.succ-n { color:#d93025; font-weight:bold; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/webhook/webhookSendLogMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/webhook/webhookSendLogMan.view" />';
|
||||
var combo;
|
||||
|
||||
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
|
||||
function escapeAttr(s) { return escapeHtmlJs(s).replace(/"/g, '"'); }
|
||||
|
||||
function formatTargetUrl(cellvalue, options, rowObject) {
|
||||
var v = cellvalue || '';
|
||||
return '<span class="url-cell" title="' + escapeAttr(v) + '">' + escapeHtmlJs(v) + '</span>';
|
||||
}
|
||||
|
||||
// 이벤트유형 코드 → 모니터링 공통코드(EVENT_TYPE) 코드명
|
||||
function formatEventType(cellvalue, options, rowObject) {
|
||||
if (!combo || !combo.eventTypeList) {
|
||||
return escapeHtmlJs(cellvalue);
|
||||
}
|
||||
for (var i = 0; i < combo.eventTypeList.length; i++) {
|
||||
if (combo.eventTypeList[i].CODE == cellvalue) {
|
||||
return escapeHtmlJs(combo.eventTypeList[i].NAME);
|
||||
}
|
||||
}
|
||||
return escapeHtmlJs(cellvalue);
|
||||
}
|
||||
|
||||
function formatSuccess(cellvalue, options, rowObject) {
|
||||
if (cellvalue === true) return '<span class="succ-y">성공</span>';
|
||||
if (cellvalue === false) return '<span class="succ-n">실패</span>';
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
function init() {
|
||||
|
||||
$("#startDatepicker").datepicker().inputmask("yyyy-mm-dd",{'autoUnmask':true});
|
||||
$("#endDatepicker").datepicker().inputmask("yyyy-mm-dd",{'autoUnmask':true});
|
||||
|
||||
var today = getToday();
|
||||
|
||||
$("input[name=searchStartYYYYMMDD]").val(today.substring(0,6)+"01");
|
||||
$("input[name=searchEndYYYYMMDD]").val(today);
|
||||
$("input[name=searchStartDate]").val(today.substring(0,6)+"01");
|
||||
$("input[name=searchEndDate]").val(today);
|
||||
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'LIST_INIT_COMBO'},
|
||||
success: function (json) {
|
||||
combo = json;
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchEventType]"))
|
||||
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
|
||||
.setData(json.eventTypeList).rendering();
|
||||
|
||||
// 뒤로가기 등으로 이전 검색조건 파라미터가 있으면 기본값을 덮어씀
|
||||
putSelectFromParam();
|
||||
|
||||
// 콤보(이벤트유형) 로드 후 그리드 생성 — formatEventType 경합 방지
|
||||
buildGrid();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function buildGrid() {
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Status', '성공여부', '등록일시', '발송일시'],
|
||||
colModel: [
|
||||
{name: 'id', align: 'center', key: true, hidden: true},
|
||||
{name: 'rowNum', align: 'center', width: 45, sortable: false},
|
||||
{name: 'orgName', align: 'left', width: 140},
|
||||
{name: 'eventType', align: 'center', width: 120, formatter: formatEventType},
|
||||
{name: 'targetUrl', align: 'left', width: 300, formatter: formatTargetUrl},
|
||||
{name: 'statusCode', align: 'center', width: 60},
|
||||
{name: 'success', align: 'center', width: 70, formatter: formatSuccess},
|
||||
{name: 'createdAt', align: 'center', width: 140},
|
||||
{name: 'sentAt', align: 'center', width: 140}
|
||||
],
|
||||
jsonReader: {repeatitems: false},
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: $("#container").height(),
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList: eval('[${rmsDefaultRowList}]'),
|
||||
ondblClickRow: function (rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var id = rowData['id'];
|
||||
var url2 = url_view + '?cmd=DETAIL';
|
||||
url2 += '&page=' + $(this).getGridParam("page");
|
||||
url2 += '&returnUrl=' + getReturnUrl();
|
||||
url2 += '&menuId=' + '${param.menuId}';
|
||||
url2 += '&id=' + id;
|
||||
goNav(url2);
|
||||
},
|
||||
loadComplete: function () {
|
||||
var page = $(this).getGridParam('page');
|
||||
var rowNum = $(this).getGridParam('rowNum');
|
||||
var rows = $(this).getDataIDs();
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var number = ((page - 1) * rowNum + i) + 1;
|
||||
$(this).setCell(rows[i], 'rowNum', number);
|
||||
}
|
||||
// 서버 고정 정렬(등록일시 역순) — 컬럼 정렬 비활성
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, {sortable: false});
|
||||
}
|
||||
},
|
||||
loadError: function (jqXHR, textStatus, errorThrown) {
|
||||
var location = '<%=request.getContextPath()%>/';
|
||||
comloadError(jqXHR, textStatus, errorThrown, location);
|
||||
}
|
||||
});
|
||||
resizeJqGridWidth('grid', 'content_middle', '1000');
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "");
|
||||
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi, "");
|
||||
|
||||
if (start && start > getToday()) {
|
||||
alert("시작일이 오늘 이후입니다. 시작일을 확인해주세요.");
|
||||
$("input[name=searchStartYYYYMMDD]").focus();
|
||||
return false;
|
||||
}
|
||||
if (start && end && start > end) {
|
||||
alert("조회기간 종료일은 시작일보다 커야합니다.");
|
||||
$("input[name=searchEndYYYYMMDD]").focus();
|
||||
return false;
|
||||
}
|
||||
$("input[name=searchStartDate]").val(start);
|
||||
$("input[name=searchEndDate]").val(end);
|
||||
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("select[name^=search]").change(function () { $("#btn_search").click(); });
|
||||
$("input[name^=search]").keydown(function (key) {
|
||||
if (key.keyCode == 13) { $("#btn_search").click(); }
|
||||
});
|
||||
|
||||
|
||||
|
||||
buttonControl();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
</div>
|
||||
<div class="title">웹훅 발송 로그<span class="tooltip">외부로 발송된 웹훅 이력을 조회합니다.</span></div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<colgroup>
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>기관명</th>
|
||||
<td><input type="text" name="searchOrgName" autocomplete="off"></td>
|
||||
<th>이벤트유형</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchEventType"></select>
|
||||
</div>
|
||||
</td>
|
||||
<th>성공여부</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchSuccess">
|
||||
<option value="">전체</option>
|
||||
<option value="Y">성공</option>
|
||||
<option value="N">실패</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Target URL</th>
|
||||
<td><input type="text" name="searchTargetUrl" autocomplete="off" style="width:95%;"></td>
|
||||
<th>등록기간</th>
|
||||
<td>
|
||||
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:40%; border:1px solid #ebebec;">
|
||||
~
|
||||
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:40%; border:1px solid #ebebec;">
|
||||
<input type="hidden" name="searchStartDate" value="">
|
||||
<input type="hidden" name="searchEndDate" value="">
|
||||
</td>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,134 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.succ-y { color:#1a73e8; font-weight:bold; }
|
||||
.succ-n { color:#d93025; font-weight:bold; }
|
||||
.pre-wrap { white-space:pre-wrap; word-break:break-all; min-height:60px; line-height:1.5; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/webhook/webhookSendLogMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/webhook/webhookSendLogMan.view" />';
|
||||
var key = '${param.id}';
|
||||
|
||||
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
|
||||
|
||||
function detail() {
|
||||
if (!key) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'DETAIL', id: key},
|
||||
success: function (data) {
|
||||
$('#id').text(data.id != null ? data.id : '');
|
||||
$('#orgName').text(data.orgName || '');
|
||||
$('#eventType').text(data.eventTypeName || data.eventType || '')
|
||||
.attr('title', data.eventType || '');
|
||||
$('#targetUrl').text(data.targetUrl || '');
|
||||
$('#statusCode').text(data.statusCode != null ? data.statusCode : '');
|
||||
$('#retryCount').text(data.retryCount != null ? data.retryCount : '');
|
||||
$('#createdAt').text(data.createdAt || '');
|
||||
$('#sentAt').text(data.sentAt || '');
|
||||
$('#signature').text(data.signature || '');
|
||||
$('#errorMessage').text(data.errorMessage || '');
|
||||
$('#payload').text(data.payload || '');
|
||||
$('#responseBody').text(data.responseBody || '');
|
||||
|
||||
if (data.success === true) {
|
||||
$('#success').html('<span class="succ-y">성공</span>');
|
||||
} else if (data.success === false) {
|
||||
$('#success').html('<span class="succ-n">실패</span>');
|
||||
} else {
|
||||
$('#success').text('');
|
||||
}
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
|
||||
buttonControl();
|
||||
detail();
|
||||
|
||||
$('#btn_previous').click(function () { goNav(returnUrl); });
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div>
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title" id="title">웹훅 발송 로그 상세</div>
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<td><span id="id"></span></td>
|
||||
<th>기관명</th>
|
||||
<td><span id="orgName"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>이벤트유형</th>
|
||||
<td><span id="eventType"></span></td>
|
||||
<th>Status Code</th>
|
||||
<td><span id="statusCode"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Target URL</th>
|
||||
<td colspan="3"><span id="targetUrl"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>성공여부</th>
|
||||
<td><span id="success"></span></td>
|
||||
<th>재시도횟수</th>
|
||||
<td><span id="retryCount"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>등록일시</th>
|
||||
<td><span id="createdAt"></span></td>
|
||||
<th>발송일시</th>
|
||||
<td><span id="sentAt"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Signature</th>
|
||||
<td colspan="3"><span id="signature"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payload</th>
|
||||
<td colspan="3"><div id="payload" class="pre-wrap"></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Response Body</th>
|
||||
<td colspan="3"><div id="responseBody" class="pre-wrap"></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>에러 메세지</th>
|
||||
<td colspan="3"><div id="errorMessage" class="pre-wrap"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -258,14 +258,17 @@ public interface MonitoringContext {
|
||||
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 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";
|
||||
// 웹훅
|
||||
public static final String RMS_WEBHOOK_REVERSE_PROXY_URL = "rms.webhook.reverse_proxy.url";
|
||||
public static final String RMS_WEBHOOK_RETRY_COUNT = "rms.webhook.retry_count";
|
||||
public static final String RMS_WEBHOOK_RETRY_TIME = "rms.webhook.retry_time";
|
||||
|
||||
//비밀번호 초기화 접미사
|
||||
public static final String RMS_PASSWORD_INIT_SUBFIX = "rms.password.init.subfix";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public abstract String getStringProperty(String propertyName);
|
||||
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.controller;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.web.SortDefault;
|
||||
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.base.BaseAnnotationController;
|
||||
import com.eactive.eai.rms.common.combo.ComboService;
|
||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendLogUI;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendLogUISearch;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookSendLogManService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 웹훅 발송 로그(PTL_WEBHOOK_SEND_LOG) 조회 화면 컨트롤러.
|
||||
*
|
||||
* <p>조회 전용 (등록/수정/삭제 없음)</p>
|
||||
*/
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookSendLogManController extends BaseAnnotationController {
|
||||
|
||||
private final WebhookSendLogManService webhookSendLogManService;
|
||||
private final ComboService comboService;
|
||||
|
||||
@GetMapping(value = "/onl/apim/webhook/webhookSendLogMan.view")
|
||||
public void view() {
|
||||
// 목록 view
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/apim/webhook/webhookSendLogMan.view", params = "cmd=DETAIL")
|
||||
public String detailView() {
|
||||
return "/onl/apim/webhook/webhookSendLogManDetail";
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/webhook/webhookSendLogMan.json", params = "cmd=LIST")
|
||||
public ResponseEntity<GridResponse<WebhookSendLogUI>> selectList(
|
||||
@SortDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
|
||||
WebhookSendLogUISearch search) {
|
||||
Page<WebhookSendLogUI> page = webhookSendLogManService.selectList(pageable, search);
|
||||
return ResponseEntity.ok(new GridResponse<>(page));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/webhook/webhookSendLogMan.json", params = "cmd=LIST_INIT_COMBO")
|
||||
public ResponseEntity<Map<String, Object>> initCombo() {
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("eventTypeList", comboService.getMonitoringCodeSortedBySeq(WebhookSendLogManService.CODE_GROUP_EVENT_TYPE));
|
||||
return ResponseEntity.ok(resultMap);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/webhook/webhookSendLogMan.json", params = "cmd=DETAIL")
|
||||
public ResponseEntity<WebhookSendLogUI> selectDetail(Long id) {
|
||||
return ResponseEntity.ok(webhookSendLogManService.selectDetail(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 웹훅 발송 로그(PTL_WEBHOOK_SEND_LOG) 화면 응답 DTO.
|
||||
*
|
||||
* <p>목록에서는 일부 항목만 사용하고, 상세에서는 전체 항목을 사용한다.
|
||||
* 날짜는 yyyyMMdd HH:mm:ss 형식 문자열로 제공한다.</p>
|
||||
*/
|
||||
@Data
|
||||
public class WebhookSendLogUI {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String orgId;
|
||||
private String orgName;
|
||||
|
||||
private String eventType;
|
||||
/** 모니터링 공통코드(EVENT_TYPE) 코드명. 상세 조회에서만 채워진다(목록은 화면에서 콤보로 변환). */
|
||||
private String eventTypeName;
|
||||
private String targetUrl;
|
||||
|
||||
private String payload;
|
||||
private String signature;
|
||||
|
||||
private Integer statusCode;
|
||||
private String responseBody;
|
||||
|
||||
private Boolean success;
|
||||
private String errorMessage;
|
||||
private Integer retryCount;
|
||||
|
||||
/* 등록/발송 시간 — yyyyMMdd HH:mm:ss */
|
||||
private String createdAt;
|
||||
private String sentAt;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WebhookSendLogUISearch {
|
||||
|
||||
/* 기관명 (ptl_org.org_name, LIKE 검색) */
|
||||
private String searchOrgName;
|
||||
|
||||
/* 이벤트 유형 (모니터링 공통코드 EVENT_TYPE, 콤보 선택 값과 정확히 일치) */
|
||||
private String searchEventType;
|
||||
|
||||
/* 수신 URL (LIKE 검색) */
|
||||
private String searchTargetUrl;
|
||||
|
||||
/* 성공여부 (Y/N, 공백=전체) */
|
||||
private String searchSuccess;
|
||||
|
||||
/* 등록일시 기간 검색 (yyyyMMdd, 8자리 — userLoginHistoryMan.jsp 기간검색 스타일) */
|
||||
private String searchStartDate;
|
||||
private String searchEndDate;
|
||||
}
|
||||
@@ -10,5 +10,6 @@ public class WebhookSendRequest {
|
||||
private String targetUrl;
|
||||
private String eventType;
|
||||
private String secret;
|
||||
private String reverseProxyPath;
|
||||
private Object data;
|
||||
}
|
||||
+2
-2
@@ -4,17 +4,17 @@ package com.eactive.eai.rms.ext.djb.webhook.repository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
import com.eactive.eai.rms.data.EMSDataSource;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
|
||||
|
||||
@Repository
|
||||
@EMSDataSource
|
||||
public interface WebhookSendLogRepository extends JpaRepository<WebhookSendLog, Long> {
|
||||
public interface WebhookSendLogRepository extends BaseRepository<WebhookSendLog, Long> {
|
||||
|
||||
// 오라클 CHAR(1) Y/N 기준 조회
|
||||
@Query("SELECT l FROM WebhookSendLog l " +
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.apim.portal.monitoringcode.entity.MonitoringCode;
|
||||
import com.eactive.apim.portal.monitoringcode.entity.MonitoringCodeId;
|
||||
import com.eactive.apim.portal.portalorg.entity.QPortalOrg;
|
||||
import com.eactive.eai.rms.common.base.BaseService;
|
||||
import com.eactive.eai.rms.common.util.StringUtils;
|
||||
import com.eactive.eai.rms.data.entity.man.monitoringCode.repository.MonitoringCodeRepository;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookSendLog;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendLogUI;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendLogUISearch;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
|
||||
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||
import com.querydsl.core.BooleanBuilder;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 웹훅 발송 로그(PTL_WEBHOOK_SEND_LOG) 조회 화면 서비스.
|
||||
*
|
||||
* <p>조회 전용. orgId 는 엔티티 연관관계가 아닌 단순 컬럼이므로,
|
||||
* 기관명은 QueryDSL 로 ptl_org 를 별도 조회하여 매핑한다.</p>
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
public class WebhookSendLogManService extends BaseService {
|
||||
|
||||
/** 모니터링 공통코드(tseairm28) 상의 웹훅 이벤트유형 코드 그룹. */
|
||||
public static final String CODE_GROUP_EVENT_TYPE = "EVENT_TYPE";
|
||||
|
||||
private static final DateTimeFormatter FMT_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter FMT_YYYYMMDD = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
private final WebhookSendLogRepository webhookSendLogRepository;
|
||||
private final MonitoringCodeRepository monitoringCodeRepository;
|
||||
|
||||
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
|
||||
private EntityManager entityManager;
|
||||
|
||||
public Page<WebhookSendLogUI> selectList(Pageable pageable, WebhookSendLogUISearch search) {
|
||||
QWebhookSendLog log = QWebhookSendLog.webhookSendLog;
|
||||
BooleanBuilder predicate = new BooleanBuilder();
|
||||
|
||||
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
|
||||
List<String> orgIds = findOrgIdsByName(search.getSearchOrgName());
|
||||
if (orgIds.isEmpty()) {
|
||||
// 일치하는 기관이 없으면 쿼리를 실행할 필요 없이 빈 결과를 바로 반환한다.
|
||||
// (QueryDSL의 where false 는 이 프로젝트의 Hibernate HQL 파서에서 지원하지 않음)
|
||||
return Page.empty(pageable);
|
||||
}
|
||||
predicate.and(log.orgId.in(orgIds));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchEventType())) {
|
||||
predicate.and(log.eventType.eq(search.getSearchEventType()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchTargetUrl())) {
|
||||
predicate.and(log.targetUrl.containsIgnoreCase(search.getSearchTargetUrl()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchSuccess())) {
|
||||
predicate.and(log.success.eq(search.getSearchSuccess()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchStartDate())) {
|
||||
predicate.and(log.createdAt.goe(parseYYYYMMDD(search.getSearchStartDate()).atStartOfDay()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchEndDate())) {
|
||||
predicate.and(log.createdAt.loe(parseYYYYMMDD(search.getSearchEndDate()).atTime(23, 59, 59)));
|
||||
}
|
||||
|
||||
Page<WebhookSendLog> page = webhookSendLogRepository.findAll(predicate, pageable);
|
||||
|
||||
Map<String, String> orgNameMap = findOrgNames(page.getContent());
|
||||
// 목록의 이벤트유형 코드명은 화면에서 콤보(LIST_INIT_COMBO) 기준으로 변환하므로 서버에서는 조회하지 않는다(N+1 방지).
|
||||
return page.map(e -> convertToUI(e, orgNameMap.get(e.getOrgId()), null));
|
||||
}
|
||||
|
||||
public WebhookSendLogUI selectDetail(Long id) {
|
||||
WebhookSendLog entity = webhookSendLogRepository.findById(id)
|
||||
.orElseThrow(() -> new BizException("존재하지 않는 웹훅 발송 로그입니다."));
|
||||
|
||||
Map<String, String> orgNameMap = findOrgNames(Collections.singletonList(entity));
|
||||
String eventTypeName = resolveEventTypeName(entity.getEventType());
|
||||
return convertToUI(entity, orgNameMap.get(entity.getOrgId()), eventTypeName);
|
||||
}
|
||||
|
||||
private static LocalDate parseYYYYMMDD(String yyyyMMdd) {
|
||||
return LocalDate.parse(yyyyMMdd, FMT_YYYYMMDD);
|
||||
}
|
||||
|
||||
/** 모니터링 공통코드(EVENT_TYPE)에서 코드명을 조회한다. 코드가 없으면 원본 코드를 그대로 반환한다. */
|
||||
private String resolveEventTypeName(String eventType) {
|
||||
if (StringUtils.isBlank(eventType)) {
|
||||
return eventType;
|
||||
}
|
||||
return monitoringCodeRepository.findById(new MonitoringCodeId(CODE_GROUP_EVENT_TYPE, eventType))
|
||||
.map(MonitoringCode::getCodeName)
|
||||
.orElse(eventType);
|
||||
}
|
||||
|
||||
/** 기관명 LIKE 검색으로 매칭되는 orgId 목록. */
|
||||
private List<String> findOrgIdsByName(String orgName) {
|
||||
QPortalOrg org = QPortalOrg.portalOrg;
|
||||
return new JPAQueryFactory(entityManager)
|
||||
.select(org.id)
|
||||
.from(org)
|
||||
.where(org.orgName.containsIgnoreCase(orgName))
|
||||
.fetch();
|
||||
}
|
||||
|
||||
/** 목록/상세에 필요한 orgId → orgName 매핑 (일괄 조회로 N+1 방지). */
|
||||
private Map<String, String> findOrgNames(List<WebhookSendLog> logs) {
|
||||
Set<String> orgIds = logs.stream()
|
||||
.map(WebhookSendLog::getOrgId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
if (orgIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
QPortalOrg org = QPortalOrg.portalOrg;
|
||||
return new JPAQueryFactory(entityManager)
|
||||
.select(org.id, org.orgName)
|
||||
.from(org)
|
||||
.where(org.id.in(orgIds))
|
||||
.fetch()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(t -> t.get(org.id), t -> t.get(org.orgName)));
|
||||
}
|
||||
|
||||
private WebhookSendLogUI convertToUI(WebhookSendLog e, String orgName, String eventTypeName) {
|
||||
WebhookSendLogUI ui = new WebhookSendLogUI();
|
||||
ui.setId(e.getId());
|
||||
ui.setOrgId(e.getOrgId());
|
||||
ui.setOrgName(orgName);
|
||||
ui.setEventType(e.getEventType());
|
||||
ui.setEventTypeName(eventTypeName);
|
||||
ui.setTargetUrl(e.getTargetUrl());
|
||||
ui.setPayload(e.getPayload());
|
||||
ui.setSignature(e.getSignature());
|
||||
ui.setStatusCode(e.getStatusCode());
|
||||
ui.setResponseBody(e.getResponseBody());
|
||||
ui.setSuccess(e.getSuccess());
|
||||
ui.setErrorMessage(e.getErrorMessage());
|
||||
ui.setRetryCount(e.getRetryCount());
|
||||
ui.setCreatedAt(fmt(e.getCreatedAt(), FMT_DATETIME));
|
||||
ui.setSentAt(fmt(e.getSentAt(), FMT_DATETIME));
|
||||
return ui;
|
||||
}
|
||||
|
||||
private static String fmt(LocalDateTime t, DateTimeFormatter f) {
|
||||
return t == null ? "" : t.format(f);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.eactive.apim.portal.portalorg.entity.QPortalOrg;
|
||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqApi;
|
||||
@@ -60,24 +61,28 @@ public class WebhookService {
|
||||
QWebhookReq req = QWebhookReq.webhookReq;
|
||||
QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent;
|
||||
QWebhookReqApi api = QWebhookReqApi.webhookReqApi;
|
||||
QPortalOrg org = QPortalOrg.portalOrg;
|
||||
|
||||
String reverseProxyUrl = monitoringContext.getProperty(MonitoringContext.RMS_WEBHOOK_REVERSE_PROXY_URL);
|
||||
|
||||
List<Tuple> tuples = new JPAQueryFactory(entityManager)
|
||||
.select(req.orgId, req.targetUrl, req.secret, api.id.apiId)
|
||||
.select(req.orgId, req.targetUrl, req.secret, org.reverseProxyPath, api.id.apiId)
|
||||
.from(req)
|
||||
.join(evt).on(req.id.eq(evt.id.webhookReqId))
|
||||
.join(api).on(req.id.eq(api.id.webhookReqId))
|
||||
.join(org).on(req.orgId.eq(org.id))
|
||||
.where(evt.id.eventType.eq(event)
|
||||
.and(api.id.apiId.in(apiIds)))
|
||||
.fetch();
|
||||
|
||||
// orgId + targetUrl + secret 기준으로 그룹핑, apiId를 data(List)로 집계
|
||||
// orgId + targetUrl + secret 기준으로 그룹핑, apiIds를 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.setTargetUrl(this.convertTargetUrl(reverseProxyUrl, t.get(org.reverseProxyPath), t.get(req.targetUrl)));
|
||||
wr.setSecret(t.get(req.secret));
|
||||
wr.setEventType(event);
|
||||
wr.setData(new ArrayList<String>());
|
||||
@@ -88,6 +93,46 @@ public class WebhookService {
|
||||
|
||||
return new ArrayList<>(resultMap.values());
|
||||
}
|
||||
|
||||
//reverse proxy 서버로 발송하기 위한 url 만든다
|
||||
private String convertTargetUrl(String proxyUrl, String path, String targetUrl) {
|
||||
String base = stripTrailingSlash(proxyUrl);
|
||||
String normalizedPath = stripSlashes(path);
|
||||
// scheme://host[:port] 부분만 제거하고 이후 path/query/fragment는 그대로 유지
|
||||
String targetPath = targetUrl.replaceFirst("^[a-zA-Z][a-zA-Z0-9+.-]*://[^/]+", "");
|
||||
|
||||
StringBuilder url = new StringBuilder(base);
|
||||
if (!normalizedPath.isEmpty()) {
|
||||
url.append('/').append(normalizedPath);
|
||||
}
|
||||
url.append(targetPath);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private String stripTrailingSlash(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
int end = value.length();
|
||||
while (end > 0 && value.charAt(end - 1) == '/') {
|
||||
end--;
|
||||
}
|
||||
return value.substring(0, end);
|
||||
}
|
||||
|
||||
private String stripSlashes(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String result = value.trim();
|
||||
while (result.startsWith("/")) {
|
||||
result = result.substring(1);
|
||||
}
|
||||
while (result.endsWith("/")) {
|
||||
result = result.substring(0, result.length() - 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 웹훅 발송 메인 메서드
|
||||
@@ -123,8 +168,8 @@ public class WebhookService {
|
||||
headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp()));
|
||||
|
||||
// 4. 발송 (재시도 포함)
|
||||
int retryCount = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_COUNT, DEFAULT_RETRY_COUNT);
|
||||
int retryTime = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME);
|
||||
int retryCount = monitoringContext.getIntProperty(MonitoringContext.RMS_WEBHOOK_RETRY_COUNT, DEFAULT_RETRY_COUNT);
|
||||
int retryTime = monitoringContext.getIntProperty(MonitoringContext.RMS_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME);
|
||||
sendLog.setSentAt(LocalDateTime.now());
|
||||
HttpEntity<String> httpRequest = new HttpEntity<>(payloadJson, headers);
|
||||
ResponseEntity<String> response = sendWithRetry(targetUrl, httpRequest, sendLog, retryCount, retryTime);
|
||||
|
||||
Reference in New Issue
Block a user