Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d916eda53 | |||
| 49fdc3b577 | |||
| e9bff78cbf | |||
| 6ccd669ec6 | |||
| 5db046eb10 | |||
| 8bd8558038 | |||
| c3f5d6a42e | |||
| 72cdb04261 | |||
| d3601c3991 | |||
| e213c96994 | |||
| 889d1fcfad | |||
| 45f980f638 |
@@ -1,5 +1,3 @@
|
||||
gradle.properties
|
||||
|
||||
# Package Files #
|
||||
*.war
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
p:render="ONL,COM"
|
||||
/>
|
||||
<!-- BAP 일괄전송FTP -->
|
||||
<bean
|
||||
<!-- <bean
|
||||
id="BAP"
|
||||
class="com.eactive.eai.rms.common.datasource.DataSourceType"
|
||||
p:name="BAP"
|
||||
@@ -42,7 +42,7 @@
|
||||
p:online="true"
|
||||
p:jndiName="jdbc/dsOBP_BAP"
|
||||
p:render="BAP,COM"
|
||||
/>
|
||||
/> -->
|
||||
<!-- RMS_DEFAULT -->
|
||||
<bean
|
||||
id="MONITORING"
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
<entry
|
||||
key="APIGW"
|
||||
value-ref="APIGW" />
|
||||
<entry
|
||||
<!-- <entry
|
||||
key="BAP"
|
||||
value-ref="BAP" />
|
||||
value-ref="BAP" /> -->
|
||||
</map>
|
||||
</property>
|
||||
<property
|
||||
@@ -91,7 +91,7 @@
|
||||
<property name="configLocations">
|
||||
<list>
|
||||
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfOnl.xml</value>
|
||||
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value>
|
||||
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value> -->
|
||||
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBat.xml</value>
|
||||
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfService.xml</value> -->
|
||||
</list>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<%@ 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"/>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
|
||||
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData: {
|
||||
cmd: 'LIST',
|
||||
searchName: $('input[name=searchName]').val(),
|
||||
algType: $('select[name=algType]').val(),
|
||||
keySourceType: $('select[name=keySourceType]').val(),
|
||||
useYn: $('select[name=useYn]').val()
|
||||
},
|
||||
colNames: ['ID', '모듈명', '설명', '알고리즘', '운영모드', '키 소스', '사용'],
|
||||
colModel: [
|
||||
{ name: 'CRYPTO_ID', hidden: true },
|
||||
{ name: 'CRYPTO_NAME', align: 'left' },
|
||||
{ name: 'CRYPTO_DESC', align: 'left', sortable: false },
|
||||
{ name: 'ALG_TYPE', align: 'center', width: 80 },
|
||||
{ name: 'CIPHER_MODE', align: 'center', width: 80 },
|
||||
{ name: 'KEY_SOURCE_TYPE', align: 'center', width: 100 },
|
||||
{ name: 'USE_YN', align: 'center', width: 60 }
|
||||
],
|
||||
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 cryptoId = rowData['CRYPTO_ID'];
|
||||
var url2 = url_view + '?cmd=DETAIL';
|
||||
url2 += '&page=' + $(this).getGridParam("page");
|
||||
url2 += '&returnUrl=' + getReturnUrl();
|
||||
url2 += '&menuId=' + '${param.menuId}';
|
||||
url2 += '&searchName=' + $("input[name=searchName]").val();
|
||||
url2 += '&cryptoId=' + cryptoId;
|
||||
goNav(url2);
|
||||
},
|
||||
gridComplete: function() {
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
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');
|
||||
|
||||
$("#btn_search").click(function() {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
postData['algType'] = $('select[name=algType]').val();
|
||||
postData['keySourceType'] = $('select[name=keySourceType]').val();
|
||||
postData['useYn'] = $('select[name=useYn]').val();
|
||||
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("#btn_new").click(function() {
|
||||
var url2 = url_view + '?cmd=DETAIL';
|
||||
url2 += '&page=' + $("#grid").getGridParam("page");
|
||||
url2 += '&returnUrl=' + getReturnUrl();
|
||||
url2 += '&menuId=' + '${param.menuId}';
|
||||
goNav(url2);
|
||||
});
|
||||
|
||||
$("input[name=searchName]").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>
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> 신규</button>
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> 검색</button>
|
||||
</div>
|
||||
<div class="title">암호화 모듈 설정</div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:100px;">모듈명</th>
|
||||
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
||||
<th style="width:100px;">알고리즘</th>
|
||||
<td>
|
||||
<select name="algType">
|
||||
<option value="">전체</option>
|
||||
<option value="AES">AES</option>
|
||||
<option value="ARIA">ARIA</option>
|
||||
</select>
|
||||
</td>
|
||||
<th style="width:100px;">키 소스</th>
|
||||
<td>
|
||||
<select name="keySourceType">
|
||||
<option value="">전체</option>
|
||||
<option value="STATIC">STATIC</option>
|
||||
<option value="DYNAMIC">DYNAMIC</option>
|
||||
</select>
|
||||
</td>
|
||||
<th style="width:80px;">사용</th>
|
||||
<td>
|
||||
<select name="useYn">
|
||||
<option value="">전체</option>
|
||||
<option value="Y">Y</option>
|
||||
<option value="N">N</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,365 @@
|
||||
<%@ 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"/>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
|
||||
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
|
||||
var isDetail = false;
|
||||
|
||||
function isValid() {
|
||||
if ($('input[name=cryptoName]').val().trim() === '') {
|
||||
alert('모듈명을 입력하세요.');
|
||||
$('input[name=cryptoName]').focus();
|
||||
return false;
|
||||
}
|
||||
if ($('select[name=algType]').val() === '') {
|
||||
alert('알고리즘을 선택하세요.');
|
||||
return false;
|
||||
}
|
||||
if ($('select[name=cipherMode]').val() === '') {
|
||||
alert('운영모드를 선택하세요.');
|
||||
return false;
|
||||
}
|
||||
var keySourceType = $('select[name=keySourceType]').val();
|
||||
if (keySourceType === '') {
|
||||
alert('키 소스 유형을 선택하세요.');
|
||||
return false;
|
||||
}
|
||||
if (keySourceType === 'STATIC') {
|
||||
if ($('input[name=encKeyHex]').val().trim() === '') {
|
||||
alert('암호화 키(Hex)를 입력하세요.');
|
||||
$('input[name=encKeyHex]').focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (keySourceType === 'DYNAMIC') {
|
||||
if ($('input[name=keyDerivStrategy]').val().trim() === '') {
|
||||
alert('키 도출 전략 FQCN을 입력하세요.');
|
||||
$('input[name=keyDerivStrategy]').focus();
|
||||
return false;
|
||||
}
|
||||
if ($('textarea[name=keyDerivParams]').val().trim() === '') {
|
||||
alert('키 도출 파라미터(JSON)를 입력하세요.');
|
||||
$('textarea[name=keyDerivParams]').focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function textToHex(text) {
|
||||
if (!text) return '';
|
||||
var encoder = new TextEncoder();
|
||||
var bytes = encoder.encode(text);
|
||||
return Array.from(bytes).map(function(b) {
|
||||
return b.toString(16).padStart(2, '0');
|
||||
}).join('').toUpperCase();
|
||||
}
|
||||
|
||||
function updateHexFromText(textEl, hexName, lenSpanId) {
|
||||
var hex = textToHex($(textEl).val());
|
||||
$('input[name=' + hexName + ']').val(hex);
|
||||
updateHexLength(hex, lenSpanId);
|
||||
}
|
||||
|
||||
function updateHexLength(hex, lenSpanId) {
|
||||
var charLen = hex ? hex.replace(/\s/g, '').length : 0;
|
||||
var byteLen = Math.floor(charLen / 2);
|
||||
var msg = charLen > 0 ? byteLen + ' bytes (' + charLen + '자)' : '';
|
||||
$('#' + lenSpanId).text(msg);
|
||||
}
|
||||
|
||||
function toggleKeySourceFields() {
|
||||
var keySourceType = $('select[name=keySourceType]').val();
|
||||
if (keySourceType === 'STATIC') {
|
||||
$('.static-section').show();
|
||||
$('.dynamic-section').hide();
|
||||
} else if (keySourceType === 'DYNAMIC') {
|
||||
$('.static-section').hide();
|
||||
$('.dynamic-section').show();
|
||||
} else {
|
||||
$('.static-section').show();
|
||||
$('.dynamic-section').show();
|
||||
}
|
||||
}
|
||||
|
||||
function detail(url, key) {
|
||||
if (!isDetail) return;
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
data: { cmd: 'DETAIL', cryptoId: key },
|
||||
success: function(data) {
|
||||
$('input[name=cryptoId]').val(data['CRYPTO_ID']);
|
||||
$('input[name=cryptoName]').val(data['CRYPTO_NAME']).attr('readonly', true);
|
||||
$('input[name=cryptoDesc]').val(data['CRYPTO_DESC']);
|
||||
$('select[name=algType]').val(data['ALG_TYPE']);
|
||||
$('select[name=cipherMode]').val(data['CIPHER_MODE']);
|
||||
$('select[name=padding]').val(data['PADDING'] || '');
|
||||
$('input[name=ivHex]').val(data['IV_HEX']);
|
||||
$('select[name=keySourceType]').val(data['KEY_SOURCE_TYPE']);
|
||||
$('input[name=encKeyHex]').val(data['ENC_KEY_HEX']);
|
||||
$('input[name=decKeyHex]').val(data['DEC_KEY_HEX']);
|
||||
$('input[name=keyDerivStrategy]').val(data['KEY_DERIV_STRATEGY']);
|
||||
$('textarea[name=keyDerivParams]').val(data['KEY_DERIV_PARAMS']);
|
||||
$('select[name=cacheYn]').val(data['CACHE_YN']);
|
||||
$('input[name=cacheTtlSec]').val(data['CACHE_TTL_SEC']);
|
||||
$('select[name=useYn]').val(data['USE_YN']);
|
||||
updateHexLength(data['ENC_KEY_HEX'], 'encKeyLen');
|
||||
updateHexLength(data['DEC_KEY_HEX'], 'decKeyLen');
|
||||
updateHexLength(data['IV_HEX'], 'ivHexLen');
|
||||
if (data['MODIFIED_BY'] || data['MODIFIED_AT']) {
|
||||
$('#span_modified_by').text(data['MODIFIED_BY'] || '');
|
||||
$('#span_modified_at').text((data['MODIFIED_AT'] || '').replace('T', ' '));
|
||||
$('#row_modified_info').show();
|
||||
}
|
||||
toggleKeySourceFields();
|
||||
},
|
||||
error: function(e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
var key = '${param.cryptoId}';
|
||||
if (key !== '' && key !== 'null') {
|
||||
isDetail = true;
|
||||
}
|
||||
detail(url, key);
|
||||
|
||||
$('select[name=keySourceType]').change(function() {
|
||||
toggleKeySourceFields();
|
||||
});
|
||||
|
||||
$('input[name=encKeyHex]').on('input', function() {
|
||||
updateHexLength($(this).val(), 'encKeyLen');
|
||||
});
|
||||
$('input[name=decKeyHex]').on('input', function() {
|
||||
updateHexLength($(this).val(), 'decKeyLen');
|
||||
});
|
||||
$('input[name=ivHex]').on('input', function() {
|
||||
updateHexLength($(this).val(), 'ivHexLen');
|
||||
});
|
||||
|
||||
toggleKeySourceFields();
|
||||
|
||||
$('#btn_modify').click(function() {
|
||||
if (!isValid()) return;
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
postData.push({ name: 'cmd', value: isDetail ? 'UPDATE' : 'INSERT' });
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
data: postData,
|
||||
success: function(data) {
|
||||
var msg = '<%= localeMessage.getString("common.saveMsg") %>';
|
||||
if (data && data.broadcastResult) {
|
||||
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
|
||||
}
|
||||
alert(msg);
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function(e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_delete').click(function() {
|
||||
if (confirm('<%= localeMessage.getString("common.confirmMsg") %>') !== true) return;
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
data: { cmd: 'DELETE', cryptoId: $('input[name=cryptoId]').val() },
|
||||
success: function(data) {
|
||||
var msg = '<%= localeMessage.getString("common.deleteMsg") %>';
|
||||
if (data && data.broadcastResult) {
|
||||
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
|
||||
}
|
||||
alert(msg);
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function(e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_previous').click(function() {
|
||||
goNav(returnUrl);
|
||||
});
|
||||
|
||||
buttonControl(isDetail);
|
||||
titleControl(isDetail);
|
||||
});
|
||||
</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_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> 삭제</button>
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> 저장</button>
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> 이전</button>
|
||||
</div>
|
||||
<div class="title">암호화 모듈 설정 상세</div>
|
||||
|
||||
<form id="ajaxForm">
|
||||
<input type="hidden" name="cryptoId" />
|
||||
<table class="table_row" cellspacing="0">
|
||||
<tr>
|
||||
<th style="width:20%;">모듈명 *</th>
|
||||
<td><input type="text" name="cryptoName" style="width:300px;" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설명</th>
|
||||
<td><input type="text" name="cryptoDesc" style="width:400px;" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>알고리즘 *</th>
|
||||
<td>
|
||||
<select name="algType">
|
||||
<option value="">선택</option>
|
||||
<option value="AES">AES</option>
|
||||
<option value="ARIA">ARIA</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>운영모드 *</th>
|
||||
<td>
|
||||
<select name="cipherMode">
|
||||
<option value="">선택</option>
|
||||
<option value="CBC">CBC</option>
|
||||
<option value="GCM">GCM</option>
|
||||
<option value="ECB">ECB</option>
|
||||
<option value="FF1">FF1</option>
|
||||
<option value="FF3-1">FF3-1</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>패딩</th>
|
||||
<td>
|
||||
<select name="padding">
|
||||
<option value="">없음</option>
|
||||
<option value="PKCS5Padding">PKCS5Padding</option>
|
||||
<option value="NoPadding">NoPadding</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>IV (Hex)</th>
|
||||
<td>
|
||||
<input type="text" name="ivHex" style="width:380px;" placeholder="HEX 직접 입력" />
|
||||
<span id="ivHexLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
|
||||
<br/>
|
||||
<input type="text" id="ivHexText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
|
||||
oninput="updateHexFromText(this, 'ivHex', 'ivHexLen')" />
|
||||
<span class="help-inline">CBC: 16바이트(32자) 필수 / GCM: 설정 시 기본 AAD로 사용</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>키 소스 유형 *</th>
|
||||
<td>
|
||||
<select name="keySourceType">
|
||||
<option value="">선택</option>
|
||||
<option value="STATIC">STATIC</option>
|
||||
<option value="DYNAMIC">DYNAMIC</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- STATIC 전용 -->
|
||||
<tr class="static-section">
|
||||
<th>암호화 키 (Hex) *</th>
|
||||
<td>
|
||||
<input type="text" name="encKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
|
||||
<span id="encKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
|
||||
<br/>
|
||||
<input type="text" id="encKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
|
||||
oninput="updateHexFromText(this, 'encKeyHex', 'encKeyLen')" />
|
||||
<span class="help-inline">AES-128: 16bytes(32자) / AES-192: 24bytes(48자) / AES-256: 32bytes(64자)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="static-section">
|
||||
<th>복호화 키 (Hex)</th>
|
||||
<td>
|
||||
<input type="text" name="decKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
|
||||
<span id="decKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
|
||||
<br/>
|
||||
<input type="text" id="decKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
|
||||
oninput="updateHexFromText(this, 'decKeyHex', 'decKeyLen')" />
|
||||
<span class="help-inline">미입력 시 암호화 키와 동일</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- DYNAMIC 전용 -->
|
||||
<tr class="dynamic-section">
|
||||
<th>키 도출 전략 (FQCN) *</th>
|
||||
<td><input type="text" name="keyDerivStrategy" style="width:500px;" /></td>
|
||||
</tr>
|
||||
<tr class="dynamic-section">
|
||||
<th>키 도출 파라미터 (JSON) *</th>
|
||||
<td><textarea name="keyDerivParams" style="width:500px; height:80px;"></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>동적 키 캐시 *</th>
|
||||
<td>
|
||||
<select name="cacheYn">
|
||||
<option value="N">N</option>
|
||||
<option value="Y">Y</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>캐시 TTL (초)</th>
|
||||
<td>
|
||||
<input type="number" name="cacheTtlSec" style="width:100px;" />
|
||||
<span class="help-inline">기본값 300</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>사용 여부 *</th>
|
||||
<td>
|
||||
<select name="useYn">
|
||||
<option value="Y">Y</option>
|
||||
<option value="N">N</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="row_modified_info" style="display:none;">
|
||||
<th>수정자 / 수정일시</th>
|
||||
<td>
|
||||
<span id="span_modified_by" style="font-weight:bold;"></span>
|
||||
<span style="margin:0 8px; color:#ccc;">|</span>
|
||||
<span id="span_modified_at" style="color:#555;"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -411,7 +411,7 @@
|
||||
function detail(url,key){
|
||||
jsonUrl = url;
|
||||
if (!isDetail)return;
|
||||
$("input[name='btnRadioSyncAsync']").attr('disabled', true);
|
||||
// $("input[name='btnRadioSyncAsync']").attr('disabled', true);
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
@@ -962,11 +962,13 @@
|
||||
headerNames = messageKeyList;
|
||||
isHeaderRouting = true;
|
||||
initHeaders(headerNames);
|
||||
$('#slideInboundRoutingRule').carousel(0);
|
||||
//$('#slideInboundRoutingRule').carousel(0);
|
||||
$('#collapseInboundRoutingInfo').collapse('show')
|
||||
setHeaderRoutingLabel();
|
||||
}else{
|
||||
isHeaderRouting = false;
|
||||
$('#slideInboundRoutingRule').carousel(1);
|
||||
$('#collapseInboundRoutingInfo').collapse('hide')
|
||||
//$('#slideInboundRoutingRule').carousel(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1572,7 +1574,7 @@
|
||||
<fieldset class="groupbox-border">
|
||||
<legend class="groupbox-border">INTERFACE TYPE</legend>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-3">
|
||||
<!-- <div class="form-group col-md-3">
|
||||
<label for="eaiBzwkDstcd"><span class="material-icons-outlined">sync</span> Sync/Async 타입</label><br>
|
||||
<div class="btn-group" id="btnGroupSyncAsync" role="group">
|
||||
<input type="radio"
|
||||
@@ -1615,7 +1617,7 @@
|
||||
<i class="bi bi-share-fill"></i> S > A
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="form-group col-md-3">
|
||||
<label for="eaiBzwkDstcd">요청/응답 구분</label><br>
|
||||
<div class="btn-group" role="group" aria-label="Basic radio toggle button group"
|
||||
@@ -1771,36 +1773,34 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-8">
|
||||
<div id="slideInboundRoutingRule" class="carousel slide" data-ride="carousel" data-interval="false">
|
||||
<div class="carousel-inner" style="margin-bottom:-20px">
|
||||
<div class="carousel-item">
|
||||
<label>
|
||||
<span class="material-icons justify-content-md-start">http</span>
|
||||
수신 라우팅 정보
|
||||
<div id="slideInboundRoutingRule">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label for="inboundRestPath">
|
||||
<span class="material-icons">link</span>
|
||||
수신 REST PATH(URL)
|
||||
</label>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" id="headerRoutingButton" class="btn btn-primary"
|
||||
data-bs-toggle="modal" data-bs-target="#routingModal">
|
||||
수정
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item active">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label for="inboundRestPath">
|
||||
<span class="material-icons">link</span>
|
||||
수신 REST PATH(URL)
|
||||
</label>
|
||||
<input type="text" class="form-control" id="inboundRestPath"
|
||||
name="inboundRestPath" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="inboundRestPath"
|
||||
name="inboundRestPath" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse mt-2" id="collapseInboundRoutingInfo">
|
||||
<div class="form-group col-md-12">
|
||||
<label>
|
||||
<span class="material-icons justify-content-md-start">http</span>
|
||||
수신 라우팅 정보
|
||||
</label>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" id="headerRoutingButton" class="btn btn-primary"
|
||||
data-bs-toggle="modal" data-bs-target="#routingModal">
|
||||
수정
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="inboundResponseMethodPathRow"
|
||||
x-show="showInboundResponse">
|
||||
<div id="inboundResponseRestPart" class="row">
|
||||
|
||||
+3
-7
@@ -15,7 +15,7 @@ def quartzVersion = "2.2.1"
|
||||
def queryDslVersion = "5.0.0"
|
||||
def hibernateVersion = "5.6.15.Final"
|
||||
|
||||
def reposiliteUrl = "http://localhost:8080"
|
||||
def nexusUrl = "https://nexus.eactive.synology.me:8090"
|
||||
//def useOnJboss = false
|
||||
|
||||
def generatedJavaDir = "$buildDir/generated/java"
|
||||
@@ -23,12 +23,8 @@ def generatedJavaDir = "$buildDir/generated/java"
|
||||
allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
url "${reposiliteUrl}/private"
|
||||
url "${nexusUrl}/repository/maven-public/"
|
||||
allowInsecureProtocol = true
|
||||
credentials {
|
||||
username = reposiliteUser
|
||||
password = reposilitePassword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +125,7 @@ dependencies {
|
||||
implementation "org.snmp4j:snmp4j:1.10.1"
|
||||
implementation "com.googlecode.json-simple:json-simple:1.1.1"
|
||||
|
||||
implementation "io.netty:netty-all:4.1.0.Final"
|
||||
implementation "io.netty:netty-all:4.1.94.Final"
|
||||
|
||||
compileOnly "org.apache.mina:mina-filter-ssl:1.1.6"
|
||||
compileOnly "org.apache.mina:mina-core:1.1.6"
|
||||
|
||||
+1
-4
@@ -1,7 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.configureondemand=true
|
||||
|
||||
reposiliteUser=admin
|
||||
reposilitePassword=BFoZsHrQU4lmJf9rruDdcsPgE0gltKfRdTjAx04IgvFhrd5q07QwMFZqeLHFv+5n
|
||||
org.gradle.configureondemand=true
|
||||
@@ -251,6 +251,11 @@ public interface MonitoringContext {
|
||||
public static final String MENU_RENDER_ADDITIONAL_SERVICES = "menu.render.additional.services";
|
||||
|
||||
public static final String KEYSTORE_UPLOAD_PATH = "keystore.upload.path";
|
||||
|
||||
// 이중 로그인 허용여부
|
||||
public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED";
|
||||
|
||||
|
||||
|
||||
public abstract String getStringProperty(String propertyName);
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.eactive.eai.rms.common.interceptor;
|
||||
|
||||
|
||||
import com.eactive.eai.rms.common.login.LoginVo;
|
||||
import com.eactive.eai.rms.common.login.SessionManager;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.common.login.LoginVo;
|
||||
import com.eactive.eai.rms.common.login.SessionManager;
|
||||
|
||||
|
||||
public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
|
||||
@@ -20,6 +23,8 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
|
||||
private static final Logger logger = Logger
|
||||
.getLogger(SessionCheckInterceptor.class);
|
||||
|
||||
@Autowired
|
||||
private MonitoringContext monitoringContext;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request,
|
||||
@@ -39,6 +44,7 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
|
||||
|
||||
//craft
|
||||
boolean isSkippable = ClassUtils.isAssignable(clazz, InterceptorSkipController.class);
|
||||
boolean isDualLogin = monitoringContext.getBooleanProperty(MonitoringContext.RMS_DUAL_LOGIN_ENABLED, true);
|
||||
|
||||
if (isSkippable) {
|
||||
valid = true;
|
||||
@@ -46,7 +52,7 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
|
||||
SessionManager.setLoginVo(loginVo); // ThreadLocal에 설정
|
||||
|
||||
// 이중 로그인 검사
|
||||
if (!SessionManager.isValidSession(request)) {
|
||||
if (!SessionManager.isValidSession(request, isDualLogin)) {
|
||||
logger.warn("Duplicate login detected for user: " + loginVo.getUserId());
|
||||
valid = false;
|
||||
} else {
|
||||
|
||||
@@ -391,20 +391,24 @@ public class MainController implements InterceptorSkipController {
|
||||
|
||||
session.setAttribute("dualLogin", "N");
|
||||
|
||||
// 이중 로그인 확인
|
||||
boolean isNewLogin = SessionManager.registerUserSession(request, dto);
|
||||
boolean isDualLogin = monitoringContext.getBooleanProperty(MonitoringContext.RMS_DUAL_LOGIN_ENABLED, true);
|
||||
|
||||
if (!isNewLogin) {
|
||||
|
||||
// 1.
|
||||
SessionManager.forceRegisterUserSession(request, dto);
|
||||
session.setAttribute("dualLogin", "Y");
|
||||
|
||||
// 또는
|
||||
// 2. 이중 로그인 에러 표시
|
||||
// model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다.");
|
||||
// return "/";
|
||||
}
|
||||
if (!isDualLogin) {
|
||||
// 이중 로그인 확인
|
||||
boolean isNewLogin = SessionManager.registerUserSession(request, dto);
|
||||
|
||||
if (!isNewLogin) {
|
||||
|
||||
// 1.
|
||||
SessionManager.forceRegisterUserSession(request, dto);
|
||||
session.setAttribute("dualLogin", "Y");
|
||||
|
||||
// 또는
|
||||
// 2. 이중 로그인 에러 표시
|
||||
// model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다.");
|
||||
// return "/";
|
||||
}
|
||||
}
|
||||
|
||||
// 세션에 로그인 정보 저장
|
||||
request.getSession().setAttribute(CommonConstants.LOGIN, dto);
|
||||
@@ -665,7 +669,8 @@ public class MainController implements InterceptorSkipController {
|
||||
|
||||
if (loginVo != null) {
|
||||
// 로그인 세션 제거
|
||||
SessionManager.removeUserSession(loginVo.getUserId());
|
||||
//SessionManager.removeUserSession(loginVo.getUserId());
|
||||
SessionManager.removeUserSessionBySessionId(request.getSession().getId()); //2026.04.29 이중로그인 로그아웃 버그 수정
|
||||
|
||||
request.getSession().removeAttribute(CommonConstants.LOGIN);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ public class SessionDestructionListener implements HttpSessionListener {
|
||||
HttpSession session = se.getSession();
|
||||
|
||||
String userId = (String) session.getAttribute("userId");
|
||||
String sessionId = session.getId();
|
||||
|
||||
// 로그인 한 사용자의 세션정보 정리
|
||||
if (userId != null) {
|
||||
@@ -34,7 +35,8 @@ public class SessionDestructionListener implements HttpSessionListener {
|
||||
logger.debug("[SessionListener] 세션만료 감지. 사용자 제거: " + userId);
|
||||
}
|
||||
|
||||
SessionManager.removeUserSession(userId);
|
||||
//SessionManager.removeUserSession(userId);
|
||||
SessionManager.removeUserSessionBySessionId(sessionId); //2026.04.29 이중로그인 로그아웃 버그 수정
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.common.util.CommonConstants;
|
||||
|
||||
//craft
|
||||
@@ -206,23 +207,38 @@ public class SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 로그인 세션을 제거 (이중로그인 버그 수정용)
|
||||
* @param sessionId 로그인한 session ID
|
||||
*/
|
||||
public static void removeUserSessionBySessionId(String sessionId) {
|
||||
loggedInUsers.entrySet().removeIf(entry -> sessionId.equals(entry.getValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 세션이 유효한 로그인 세션인지 확인
|
||||
* @param request HTTP 요청
|
||||
* @param isDualLogin 이중 로그인 허용여부
|
||||
* @return true: 유효한 세션, false: 유효하지 않은 세션
|
||||
*/
|
||||
public static boolean isValidSession(HttpServletRequest request) {
|
||||
public static boolean isValidSession(HttpServletRequest request, boolean isDualLogin) {
|
||||
LoginVo loginVo = getLoginVo(request);
|
||||
if (loginVo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String userId = loginVo.getUserId();
|
||||
String currentSessionId = request.getSession().getId();
|
||||
String registeredSessionId = loggedInUsers.get(userId);
|
||||
|
||||
// 등록된 세션이 없거나 현재 세션과 다른 경우
|
||||
return registeredSessionId != null && registeredSessionId.equals(currentSessionId);
|
||||
}
|
||||
if (isDualLogin) {
|
||||
return true;
|
||||
} else {
|
||||
String userId = loginVo.getUserId();
|
||||
String currentSessionId = request.getSession().getId();
|
||||
String registeredSessionId = loggedInUsers.get(userId);
|
||||
|
||||
// 등록된 세션이 없거나 현재 세션과 다른 경우
|
||||
return registeredSessionId != null && registeredSessionId.equals(currentSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.security;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import com.eactive.eai.data.DataService;
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
|
||||
public interface CryptoModuleConfigDataService extends DataService<CryptoModuleConfig, String> {
|
||||
|
||||
Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType, String keySourceType, String useYn);
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.security;
|
||||
|
||||
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.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
import com.eactive.eai.data.entity.onl.security.QCryptoModuleConfig;
|
||||
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class CryptoModuleConfigDataServiceImpl
|
||||
extends AbstractDataService<CryptoModuleConfig, String, CryptoModuleConfigRepository>
|
||||
implements CryptoModuleConfigDataService {
|
||||
|
||||
@Override
|
||||
public Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType,
|
||||
String keySourceType, String useYn) {
|
||||
QCryptoModuleConfig q = QCryptoModuleConfig.cryptoModuleConfig;
|
||||
|
||||
BooleanExpression predicate = q.cryptoName.containsIgnoreCase(searchName != null ? searchName : "");
|
||||
|
||||
if (algType != null && !algType.isEmpty()) {
|
||||
predicate = predicate.and(q.algType.eq(algType));
|
||||
}
|
||||
if (keySourceType != null && !keySourceType.isEmpty()) {
|
||||
predicate = predicate.and(q.keySourceType.eq(keySourceType));
|
||||
}
|
||||
if (useYn != null && !useYn.isEmpty()) {
|
||||
predicate = predicate.and(q.useYn.eq(useYn));
|
||||
}
|
||||
|
||||
return repository.findAll(predicate, pageable);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.security;
|
||||
|
||||
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
|
||||
interface CryptoModuleConfigRepository
|
||||
extends BaseRepository<CryptoModuleConfig, String>, QuerydslPredicateExecutor<CryptoModuleConfig> {
|
||||
}
|
||||
+1
-1
@@ -26,7 +26,7 @@ public class ExtendedColumnDefinitionService
|
||||
public List<ExtendedColumnDefinition> findAll() {
|
||||
QExtendedColumnDefinition q = QExtendedColumnDefinition.extendedColumnDefinition;
|
||||
List<ExtendedColumnDefinition> list = new ArrayList<>();
|
||||
repository.findAll(q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add);
|
||||
repository.findAll(q.orderSeq.asc(), q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add);
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.eactive.eai.rms.data.ext.djb.apistatus;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
|
||||
import com.eactive.eai.data.entity.AbstractEntity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
|
||||
@Entity
|
||||
@Table(name = "API_STATUS")
|
||||
public class ApiStatus extends AbstractEntity<String> {
|
||||
|
||||
@Id
|
||||
@Column(name = "EAISVCNAME", length = 100)
|
||||
private String eaisvcname;
|
||||
|
||||
@Column(name = "STATUS_CODE", length = 1)
|
||||
private String statusCode;
|
||||
|
||||
@LastModifiedBy // ← EMSAuditorAware가 자동으로 채움
|
||||
@Column(name = "MOD_USERID", length = 50)
|
||||
private String modUserid = "SCHEDULER";
|
||||
|
||||
@LastModifiedDate // ← save() 시 자동으로 sysdate 채움
|
||||
@Column(name = "MOD_DATE")
|
||||
private LocalDateTime modDate;
|
||||
|
||||
@Override
|
||||
public @NonNull String getId() { return eaisvcname; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.eactive.eai.rms.data.ext.djb.apistatus;
|
||||
|
||||
public interface ApiStatusEvent {
|
||||
String getEaisvcname();
|
||||
String getEvent();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.eactive.eai.rms.data.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;
|
||||
|
||||
public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
||||
|
||||
/**
|
||||
* API 상태 판단. 정상(N), 점검(C), 지연(D), 장애(E)
|
||||
*
|
||||
* 1. 정상/지연/장애 -> 점검중 : CONTROL_START
|
||||
* 2. 점검 -> 점검X : CONTROL_END
|
||||
* 3. 정상/지연 -> 장애 : ERROR_START
|
||||
* 4. 장애 -> 장애X : ERROR_END
|
||||
* 5. 정상 -> 지연 : DELAY_START
|
||||
* 6. 지연 -> 지연X : DELAY_END
|
||||
* 7. 기타 : STAY
|
||||
*/
|
||||
@Query(nativeQuery = true, value =
|
||||
" SELECT EAISVCNAME, EVENT"
|
||||
+ " FROM ("
|
||||
+ " SELECT EAISVCNAME"
|
||||
+ " , 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'"
|
||||
+ " WHEN STATUS_CODE = 'E' AND ERR_YN = 'N' THEN 'ERROR_END'"
|
||||
+ " WHEN STATUS_CODE = 'N' AND DELAY_YN = 'Y' THEN 'DELAY_START'"
|
||||
+ " WHEN STATUS_CODE = 'D' AND DELAY_YN = 'N' THEN 'DELAY_END'"
|
||||
+ " 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"
|
||||
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
|
||||
+ " 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"
|
||||
+ " 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"
|
||||
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
|
||||
+ " WHEN RESP_SUM / TOTAL > :delayAvgRespTime 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"
|
||||
+ " , SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME) AS RESP_SUM"
|
||||
+ " 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"
|
||||
+ " FROM TSEAIHE01 A LEFT OUTER JOIN API_STATUS B ON A.EAISVCNAME = B.EAISVCNAME"
|
||||
+ " )"
|
||||
+ " ) WHERE EVENT != 'STAY'")
|
||||
List<ApiStatusEvent> findApiStatusEvents(
|
||||
@Param("errorRate") int errorRate,
|
||||
@Param("errorRangeMinute") int errorRangeMinute,
|
||||
@Param("delayRangeMinute") int delayRangeMinute,
|
||||
@Param("delayAvgRespTime") int delayAvgRespTime
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class ApiStatusService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
|
||||
|
||||
@Autowired
|
||||
private ApiStatusRepository apiStatusRepository;
|
||||
|
||||
|
||||
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.getEvent(), newStatusCode);
|
||||
|
||||
//TODO: 내부직원 알림발송 & 제휴사 webhook 발송
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.eactive.eai.rms.data.ext.djb.inflow;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.EmbeddedId;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import com.eactive.eai.data.entity.AbstractEntity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
|
||||
@Entity
|
||||
@Table(name = "TSEAIFR11")
|
||||
public class InflowTokenFailLog extends AbstractEntity<InflowTokenFailLogId> implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@EmbeddedId
|
||||
@EqualsAndHashCode.Include
|
||||
private InflowTokenFailLogId id;
|
||||
|
||||
@Column(name = "EAISEVRINSTNCNAME", length = 20)
|
||||
private String eaisevrinstncname;
|
||||
|
||||
@Column(name = "EAISVCNAME", length = 30)
|
||||
private String eaisvcname;
|
||||
|
||||
@Column(name = "ADPTRBZWKGROUPNAME", length = 50)
|
||||
private String adptrbzwkgroupname;
|
||||
|
||||
@Column(name = "THRESHOLDPERSECOND")
|
||||
private Long thresholdpersecond;
|
||||
|
||||
@Column(name = "THRESHOLD")
|
||||
private Long threshold;
|
||||
|
||||
@Column(name = "THRESHOLDTIMEUNIT", length = 12)
|
||||
private String thresholdtimeunit;
|
||||
|
||||
@Override
|
||||
public @NonNull InflowTokenFailLogId getId() { return id; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.eactive.eai.rms.data.ext.djb.inflow;
|
||||
|
||||
import java.io.Serializable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Embeddable;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Embeddable
|
||||
public class InflowTokenFailLogId implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Column(name = "EAIBZWKDSTCD", length = 4)
|
||||
private String eaibzwkdstcd;
|
||||
|
||||
@Column(name = "MSGDPSTYMS", length = 17)
|
||||
private String msgdpstyms;
|
||||
|
||||
@Column(name = "EAISVCSERNO", length = 41)
|
||||
private String eaisvcserno;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.eactive.eai.rms.data.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;
|
||||
|
||||
public interface InflowTokenFailRepository extends BaseRepository<InflowTokenFailLog, InflowTokenFailLogId> {
|
||||
|
||||
@Query(nativeQuery = true, value =
|
||||
" SELECT EAISVCNAME" +
|
||||
" , COUNT(*) AS CNT" +
|
||||
" FROM TSEAIFR11" +
|
||||
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')" +
|
||||
" GROUP BY EAISVCNAME")
|
||||
List<InflowTokenFailSummary> countByEaisvcname(@Param("rangeMinute") long rangeMinute);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.eactive.eai.rms.data.ext.djb.inflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class InflowTokenFailService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(InflowTokenFailService.class);
|
||||
|
||||
@Autowired
|
||||
private InflowTokenFailRepository inflowTokenFailRepository;
|
||||
|
||||
public void checkRecentFails(long rangeMinute) {
|
||||
List<InflowTokenFailSummary> rows = inflowTokenFailRepository.countByEaisvcname(rangeMinute);
|
||||
|
||||
for (InflowTokenFailSummary summary : rows) {
|
||||
log.debug("유량제어 토큰 획득 실패: {} 최근 {}분 동안 {}건", summary.getEaisvcname(), rangeMinute, summary.getCnt());
|
||||
|
||||
//TOOD: 내부직원 알림 발송
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.eactive.eai.rms.data.ext.djb.inflow;
|
||||
|
||||
public interface InflowTokenFailSummary {
|
||||
String getEaisvcname();
|
||||
Long getCnt();
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.eactive.eai.rms.data.ext.djb.webhook;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "PTL_WEBHOOK_SEND_LOG")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class WebhookSendLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "webhook_send_log_seq")
|
||||
@SequenceGenerator(
|
||||
name = "webhook_send_log_seq",
|
||||
sequenceName = "SEQ_PTL_WEBHOOK_SEND_LOG",
|
||||
allocationSize = 1
|
||||
)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "TARGET_URL", nullable = false, length = 500)
|
||||
private String targetUrl;
|
||||
|
||||
@Column(name = "EVENT_TYPE", length = 100)
|
||||
private String eventType;
|
||||
|
||||
@Lob
|
||||
@Column(name = "PAYLOAD")
|
||||
private String payload;
|
||||
|
||||
@Column(name = "SIGNATURE", length = 500)
|
||||
private String signature;
|
||||
|
||||
@Column(name = "STATUS_CODE")
|
||||
private Integer statusCode;
|
||||
|
||||
@Lob
|
||||
@Column(name = "RESPONSE_BODY")
|
||||
private String responseBody;
|
||||
|
||||
@Column(name = "SUCCESS", length = 1)
|
||||
private String success; // 오라클 CHAR(1) : 'Y' / 'N'
|
||||
|
||||
@Lob
|
||||
@Column(name = "ERROR_MESSAGE")
|
||||
private String errorMessage;
|
||||
|
||||
@Column(name = "RETRY_COUNT")
|
||||
private Integer retryCount = 0;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "CREATED_AT", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "SENT_AT")
|
||||
private LocalDateTime sentAt;
|
||||
|
||||
/* Boolean 편의 메서드 */
|
||||
public void setSuccess(Boolean success) {
|
||||
this.success = (success != null && success) ? "Y" : "N";
|
||||
}
|
||||
|
||||
public Boolean getSuccess() {
|
||||
return "Y".equalsIgnoreCase(this.success);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package com.eactive.eai.rms.ext.djb.job;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
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.onl.common.util.DateUtil;
|
||||
|
||||
/**
|
||||
* Job - Quartz Job
|
||||
* API_STATS_MINUTE 데이터를 1분마다 조회하여 Api 상태를 판단하여 API_STATUS 테이블을 insert/update 한다.
|
||||
* & api 상태가 변경된 경우, 알림 테이블에 저장한다
|
||||
*
|
||||
* <p>Cron Schedule 권장:</p>
|
||||
* <ul>
|
||||
* <li>기본: 0 * * * * ? (매분 1회 실행)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Job 파라미터 (JobDataMap):</p>
|
||||
* <ul>
|
||||
* <li>
|
||||
* <b>api.status.error.range_minute</b>: API 장애기준 판단 시간간격 (단위: 분, 기본값: 1)
|
||||
* <b>api.status.error.rate</b>: API 장애기준 오류건수 비율(%) (단위: 시간, 기본값: 100)
|
||||
* <b>api.status.delay.range_minute</b>: API 지연기준 판단 시간간격 (단위: 분, 기본값: 1)
|
||||
* <b>api.status.delay.avg_resp_time</b>: API 지연기준 평균응답시간 (단위: 밀리세컨드, 기본값: 10000)
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@DisallowConcurrentExecution
|
||||
public class ApiStatusUpdateJob implements Job {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiStatusUpdateJob.class);
|
||||
|
||||
/** Job 파라미터 키: API 장애기준 시간간격(분) */
|
||||
public static final String KEY_API_STATUS_ERROR_RANGE_MINUTE = "api.status.error.range_minute";
|
||||
|
||||
/** Job 파라미터 키: API 장애기준 오류건수 비율(%) */
|
||||
public static final String KEY_API_STATUS_ERROR_RATE = "api.status.error.rate";
|
||||
|
||||
/** Job 파라미터 키: API 지연기준 시간간격(분) */
|
||||
public static final String KEY_API_STATUS_DELAY_RANGE_MINUTE = "api.status.delay.range_minute";
|
||||
|
||||
/** Job 파라미터 키: API 지연기준 평균응답시간(ms) */
|
||||
public static final String KEY_API_STATUS_DELAY_AVG_RESP_TIME = "api.status.delay.avg_resp_time";
|
||||
|
||||
public static final String DEFAULT_ERROR_RANGE_MINUTE = "1";
|
||||
public static final String DEFAULT_ERROR_RATE = "100";
|
||||
public static final String DEFAULT_DELAY_RANGE_MINUTE = "1";
|
||||
public static final String DEFAULT_DELAY_AVG_RESP_TIME = "10000";
|
||||
|
||||
private transient MonitoringContext monitoringContext;
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||
log.info("*** START ApiStatusUpdateJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||
|
||||
// Job 파라미터 로깅
|
||||
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
|
||||
logJobParameters(jobDataMap);
|
||||
|
||||
HashMap<String, String> param = this.checkParameters(jobDataMap);
|
||||
|
||||
|
||||
ApplicationContext appContext;
|
||||
try {
|
||||
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
|
||||
} catch (SchedulerException e) {
|
||||
log.error("applicationContext get module error", e);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
} finally {
|
||||
DataSourceContextHolder.clearDataSourceType();
|
||||
}
|
||||
|
||||
log.info("*** END ApiStatusUpdateJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 파라미터 로깅
|
||||
*/
|
||||
private void logJobParameters(JobDataMap jobDataMap) {
|
||||
if (jobDataMap == null || jobDataMap.isEmpty()) {
|
||||
log.debug("Job 파라미터 없음 (기본값 사용)");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("Job 파라미터: ");
|
||||
for (String key : jobDataMap.getKeys()) {
|
||||
sb.append(key).append("=").append(jobDataMap.getString(key)).append(", ");
|
||||
}
|
||||
log.info(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 범위 시간 파라미터 파싱
|
||||
*
|
||||
* @param jobDataMap Job 파라미터 맵
|
||||
* @return 집계 범위 시간 (파싱 실패 시 기본값 반환)
|
||||
*/
|
||||
private HashMap<String,String> checkParameters(JobDataMap jobDataMap) {
|
||||
|
||||
HashMap<String, String> param = new HashMap<String, String>();
|
||||
|
||||
if (jobDataMap == null) {
|
||||
param.put("errorRangeMinute", DEFAULT_ERROR_RANGE_MINUTE); //1분동안
|
||||
param.put("errorRate", DEFAULT_ERROR_RATE); //에러가 100% 발생시 '장애'로 판단
|
||||
param.put("delayRangeMinute", DEFAULT_DELAY_RANGE_MINUTE); //1분동안
|
||||
param.put("delayAvgRespTime", DEFAULT_DELAY_AVG_RESP_TIME); //평균 응답속도가 10초 이상이면 '지연'으로 판단
|
||||
return param;
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_ERROR_RANGE_MINUTE))) {
|
||||
param.put("errorRangeMinute", DEFAULT_ERROR_RANGE_MINUTE);
|
||||
} else {
|
||||
param.put("errorRangeMinute", jobDataMap.getString(KEY_API_STATUS_ERROR_RANGE_MINUTE));
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_ERROR_RATE))) {
|
||||
param.put("errorRate", DEFAULT_ERROR_RATE);
|
||||
} else {
|
||||
param.put("errorRate", jobDataMap.getString(KEY_API_STATUS_ERROR_RATE));
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_DELAY_RANGE_MINUTE))) {
|
||||
param.put("delayRangeMinute", DEFAULT_DELAY_RANGE_MINUTE);
|
||||
} else {
|
||||
param.put("delayRangeMinute", jobDataMap.getString(KEY_API_STATUS_DELAY_RANGE_MINUTE));
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_DELAY_AVG_RESP_TIME))) {
|
||||
param.put("delayAvgRespTime", DEFAULT_DELAY_AVG_RESP_TIME);
|
||||
} else {
|
||||
param.put("delayAvgRespTime", jobDataMap.getString(KEY_API_STATUS_DELAY_AVG_RESP_TIME));
|
||||
}
|
||||
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개발 모드 여부 확인
|
||||
* @return eai.systemmode=D 이면 true
|
||||
*/
|
||||
private boolean isDevMode() {
|
||||
String systemMode = System.getProperty("eai.systemmode", "");
|
||||
return "D".equalsIgnoreCase(systemMode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.eactive.eai.rms.ext.djb.job;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.quartz.Trigger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
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.InflowTokenFailService;
|
||||
import com.eactive.eai.rms.onl.common.util.DateUtil;
|
||||
|
||||
/**
|
||||
* Job - Quartz Job
|
||||
* TSEAIFR11 데이터를 10분마다 조회하여 데이터가 있는 경우(유량제어 토큰 획득 실패), 알림을 발송한다
|
||||
*
|
||||
* <p>Cron Schedule 권장:</p>
|
||||
* <ul>
|
||||
* <li>기본: 0 0/10 * * * ? (매10분 1회 실행)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Job 파라미터 (JobDataMap):</p>
|
||||
* <ul>
|
||||
* <li>
|
||||
* NONE
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
*/
|
||||
|
||||
@DisallowConcurrentExecution
|
||||
public class InflowTokenFailMonitorJob implements Job {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(InflowTokenFailMonitorJob.class);
|
||||
|
||||
private transient MonitoringContext monitoringContext;
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||
|
||||
long execMinute = 1; //default 배치 실행주기(분)
|
||||
|
||||
Trigger trigger = context.getTrigger();
|
||||
Date prevFireTime = trigger.getPreviousFireTime();
|
||||
Date nextFireTime = trigger.getNextFireTime();
|
||||
|
||||
//1회 이상 배치가 실행된 경우, 배치 간격을 구하여 유량제어조회 구간 파라미터로 전달한다
|
||||
if (prevFireTime != null && nextFireTime != null) {
|
||||
long diffMillis = nextFireTime.getTime() - prevFireTime.getTime();
|
||||
execMinute = TimeUnit.MILLISECONDS.toMinutes(diffMillis);
|
||||
log.info("실행 주기: {}분", execMinute);
|
||||
}
|
||||
|
||||
ApplicationContext appContext;
|
||||
try {
|
||||
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
|
||||
} catch (SchedulerException e) {
|
||||
log.error("applicationContext get module error", e);
|
||||
return;
|
||||
}
|
||||
|
||||
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
|
||||
InflowTokenFailService service = appContext.getBean(InflowTokenFailService.class);
|
||||
|
||||
DataSourceContextHolder.setDataSourceType(
|
||||
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
|
||||
try {
|
||||
service.checkRecentFails(execMinute);
|
||||
} catch (Exception e) {
|
||||
log.error("InflowTokenFailMonitorJob execution failed", e);
|
||||
throw new JobExecutionException(e);
|
||||
} finally {
|
||||
DataSourceContextHolder.clearDataSourceType();
|
||||
}
|
||||
|
||||
log.info("*** END InflowTokenFailMonitorJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 개발 모드 여부 확인
|
||||
* @return eai.systemmode=D 이면 true
|
||||
*/
|
||||
private boolean isDevMode() {
|
||||
String systemMode = System.getProperty("eai.systemmode", "");
|
||||
return "D".equalsIgnoreCase(systemMode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.eactive.eai.rms.ext.djb.statistics;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
|
||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService;
|
||||
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
||||
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
|
||||
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
|
||||
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
|
||||
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class ApiUseStatsController {
|
||||
|
||||
private final ApiStatsDayService service;
|
||||
private final ApiStatsUIMapper mapper;
|
||||
private final ApiStatsExcelExportService excelExportService;
|
||||
|
||||
@GetMapping(value = "/onl/djb/statistics/apiUseStatsMan.view")
|
||||
public String view() {
|
||||
return "/onl/djb/statistics/apiUseStatsMan";
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/djb/statistics/apiUseStatsMan.json", params = "cmd=LIST")
|
||||
public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) {
|
||||
Pageable sortedPageable = PageRequest.of(
|
||||
pageable.getPageNumber(),
|
||||
pageable.getPageSize(),
|
||||
Sort.by(Sort.Order.desc("statTime")));
|
||||
|
||||
Page<ApiStatsDay> page = service.selectList(search, sortedPageable);
|
||||
Page<ApiStatsUI> uiPage = page.map(mapper::toVo);
|
||||
|
||||
return ResponseEntity.ok(new GridResponse<>(uiPage));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/djb/statistics/apiStatsDayMan.json", params = "cmd=EXCEL_EXPORT")
|
||||
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
||||
log.info("Excel export started - search: {}", search);
|
||||
|
||||
try {
|
||||
List<ApiStatsDay> dataList = service.selectListForExcel(search);
|
||||
log.info("Data retrieved - count: {}", dataList.size());
|
||||
|
||||
if (dataList.isEmpty()) {
|
||||
log.warn("No data found for export");
|
||||
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||
response.setContentType("application/json; charset=UTF-8");
|
||||
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
|
||||
response.getWriter().flush();
|
||||
return;
|
||||
}
|
||||
|
||||
List<ApiStatsUI> uiList = dataList.stream()
|
||||
.map(mapper::toVo)
|
||||
.collect(Collectors.toList());
|
||||
log.info("Data converted to UI list - count: {}", uiList.size());
|
||||
|
||||
String fileName = generateFileName(search);
|
||||
log.info("Generating Excel file: {}", fileName);
|
||||
|
||||
excelExportService.exportApiStats(uiList, fileName, response);
|
||||
log.info("Excel export completed successfully");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Excel export failed - error: {}", e.getMessage(), e);
|
||||
|
||||
if (!response.isCommitted()) {
|
||||
response.reset();
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.setContentType("application/json; charset=UTF-8");
|
||||
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
|
||||
response.getWriter().flush();
|
||||
} else {
|
||||
log.error("Cannot send error response - response already committed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String generateFileName(ApiStatsSearch search) {
|
||||
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime())
|
||||
? search.getSearchStartDateTime()
|
||||
: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
return "API사용현황_" + timeRange + ".xlsx";
|
||||
}
|
||||
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.controller;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
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.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;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/webhook")
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookSendController {
|
||||
|
||||
private final WebhookService webhookService;
|
||||
private final WebhookReceiveService webhookReceiveService;
|
||||
|
||||
private static final int KEY_BYTE_LENGTH = 32; // 256bit
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 발송 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
@PostMapping("/send.json")
|
||||
public ResponseEntity<Map<String, Object>> sendWebhook(
|
||||
@RequestBody WebhookSendRequest request) {
|
||||
|
||||
WebhookSendLog result = webhookService.send(
|
||||
request.getTargetUrl(),
|
||||
request.getEventType(),
|
||||
request.getData()
|
||||
);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("logId", result.getId());
|
||||
response.put("success", result.getSuccess());
|
||||
response.put("statusCode", result.getStatusCode());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 수신 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* 웹훅 수신 엔드포인트
|
||||
*
|
||||
* 헤더 예시:
|
||||
* X-Webhook-Signature : sha256={hmac값}
|
||||
* X-Webhook-Event : ORDER_CREATED
|
||||
* X-Webhook-Timestamp : 1712345678901
|
||||
*/
|
||||
@PostMapping("/receive")
|
||||
public ResponseEntity<Map<String, Object>> receiveWebhook(
|
||||
@RequestHeader(value = "X-Webhook-Signature", required = false) String signature,
|
||||
@RequestHeader(value = "X-Webhook-Event", required = false) String eventType,
|
||||
@RequestHeader(value = "X-Webhook-Timestamp", required = false) String timestamp,
|
||||
@RequestBody String rawPayload) {
|
||||
|
||||
log.info("[Webhook] 수신 - eventType: {}, timestamp: {}", eventType, timestamp);
|
||||
|
||||
// 1. 필수 헤더 누락 체크
|
||||
if (signature == null || eventType == null || timestamp == null) {
|
||||
log.warn("[Webhook] 수신 거부 - 필수 헤더 누락");
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.BAD_REQUEST)
|
||||
.body(errorResponse("필수 헤더가 누락되었습니다."));
|
||||
}
|
||||
|
||||
// 2. 서명 검증
|
||||
boolean isValid = webhookReceiveService.verifySignature(rawPayload, signature);
|
||||
if (!isValid) {
|
||||
log.warn("[Webhook] 수신 거부 - 서명 불일치 / eventType: {}", eventType);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(errorResponse("서명 검증에 실패하였습니다."));
|
||||
}
|
||||
|
||||
// 3. 타임스탬프 유효성 검증 (5분 이내 요청만 허용)
|
||||
boolean isTimestampValid = webhookReceiveService.verifyTimestamp(timestamp);
|
||||
if (!isTimestampValid) {
|
||||
log.warn("[Webhook] 수신 거부 - 타임스탬프 만료 / eventType: {}", eventType);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(errorResponse("요청이 만료되었습니다."));
|
||||
}
|
||||
|
||||
// 4. 이벤트 처리
|
||||
try {
|
||||
webhookReceiveService.process(eventType, rawPayload);
|
||||
} catch (Exception e) {
|
||||
log.error("[Webhook] 이벤트 처리 실패 - eventType: {}, error: {}", eventType, e.getMessage(), e);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(errorResponse("이벤트 처리 중 오류가 발생하였습니다."));
|
||||
}
|
||||
|
||||
// 5. 정상 응답
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("result", "OK");
|
||||
response.put("eventType", eventType);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 공통 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private Map<String, Object> errorResponse(String message) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("result", "ERROR");
|
||||
error.put("message", message);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WebhookPayload {
|
||||
|
||||
private String eventType;
|
||||
private String eventId;
|
||||
private long timestamp;
|
||||
private Object data;
|
||||
|
||||
public static WebhookPayload of(String eventType, Object data) {
|
||||
return WebhookPayload.builder()
|
||||
.eventType(eventType)
|
||||
.eventId(UUID.randomUUID().toString())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(data)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class WebhookSendRequest {
|
||||
|
||||
private String targetUrl;
|
||||
private String eventType;
|
||||
private Object data;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
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.rms.data.EMSDataSource;
|
||||
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
|
||||
|
||||
@Repository
|
||||
@EMSDataSource
|
||||
public interface WebhookSendLogRepository extends JpaRepository<WebhookSendLog, Long> {
|
||||
|
||||
// 오라클 CHAR(1) Y/N 기준 조회
|
||||
@Query("SELECT l FROM WebhookSendLog l " +
|
||||
"WHERE l.success = 'N' AND l.retryCount < :maxRetry")
|
||||
List<WebhookSendLog> findFailedLogs(@Param("maxRetry") int maxRetry);
|
||||
|
||||
@Query("SELECT l FROM WebhookSendLog l " +
|
||||
"WHERE l.eventType = :eventType " +
|
||||
"AND l.createdAt BETWEEN :from AND :to")
|
||||
List<WebhookSendLog> findByEventTypeAndPeriod(
|
||||
@Param("eventType") String eventType,
|
||||
@Param("from") LocalDateTime from,
|
||||
@Param("to") LocalDateTime to);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookReceiveService {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
|
||||
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf";
|
||||
|
||||
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||
private static final long TIMESTAMP_LIMIT = 5 * 60 * 1000L; // 5분 (ms)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 서명 검증 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* 수신된 서명과 payload 로 재계산한 서명을 비교
|
||||
* 헤더값 형식 : "sha256={hex값}"
|
||||
*/
|
||||
public boolean verifySignature(String rawPayload, String receivedSignature) {
|
||||
try {
|
||||
// "sha256=" 접두어 제거
|
||||
String receivedHex = receivedSignature.startsWith("sha256=")
|
||||
? receivedSignature.substring(7)
|
||||
: receivedSignature;
|
||||
|
||||
String expectedHex = generateSignature(rawPayload);
|
||||
|
||||
// 타이밍 공격 방지 : MessageDigest.isEqual 사용
|
||||
return MessageDigest.isEqual(
|
||||
expectedHex.getBytes(StandardCharsets.UTF_8),
|
||||
receivedHex.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("[Webhook] 서명 검증 중 오류 발생", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 타임스탬프 검증 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* 요청 타임스탬프가 현재 시각 기준 5분 이내인지 확인 (Replay Attack 방지)
|
||||
*/
|
||||
public boolean verifyTimestamp(String timestampStr) {
|
||||
try {
|
||||
long requestTime = Long.parseLong(timestampStr);
|
||||
long now = System.currentTimeMillis();
|
||||
return Math.abs(now - requestTime) <= TIMESTAMP_LIMIT;
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("[Webhook] 타임스탬프 파싱 실패: {}", timestampStr);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 이벤트 처리 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* eventType 에 따라 분기 처리
|
||||
*/
|
||||
public void process(String eventType, String rawPayload) throws Exception {
|
||||
JsonNode root = objectMapper.readTree(rawPayload);
|
||||
JsonNode data = root.path("data");
|
||||
|
||||
log.info("[Webhook] 이벤트 처리 시작 - eventType: {}", eventType);
|
||||
|
||||
switch (eventType) {
|
||||
case "ORDER_CREATED":
|
||||
handleOrderCreated(data);
|
||||
break;
|
||||
case "ORDER_CANCELLED":
|
||||
handleOrderCancelled(data);
|
||||
break;
|
||||
case "PAYMENT_COMPLETED":
|
||||
handlePaymentCompleted(data);
|
||||
break;
|
||||
default:
|
||||
log.warn("[Webhook] 처리되지 않은 이벤트 - eventType: {}", eventType);
|
||||
break;
|
||||
}
|
||||
|
||||
log.info("[Webhook] 이벤트 처리 완료 - eventType: {}", eventType);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 이벤트별 핸들러 (비즈니스 로직 구현) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private void handleOrderCreated(JsonNode data) {
|
||||
log.info("[Webhook] ORDER_CREATED 처리 - data: {}", data);
|
||||
// TODO: 주문 생성 처리 로직
|
||||
}
|
||||
|
||||
private void handleOrderCancelled(JsonNode data) {
|
||||
log.info("[Webhook] ORDER_CANCELLED 처리 - data: {}", data);
|
||||
// TODO: 주문 취소 처리 로직
|
||||
}
|
||||
|
||||
private void handlePaymentCompleted(JsonNode data) {
|
||||
log.info("[Webhook] PAYMENT_COMPLETED 처리 - data: {}", data);
|
||||
// TODO: 결제 완료 처리 로직
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 공통 유틸 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private String generateSignature(String payload) throws Exception {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(
|
||||
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
||||
mac.init(keySpec);
|
||||
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hash);
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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.ext.djb.webhook.dto.WebhookPayload;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookService {
|
||||
|
||||
private final WebhookSendLogRepository sendLogRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
|
||||
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf";
|
||||
|
||||
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||
private static final int MAX_RETRY = 3;
|
||||
|
||||
/**
|
||||
* 웹훅 발송 메인 메서드
|
||||
*/
|
||||
@Transactional
|
||||
public WebhookSendLog send(String targetUrl, String eventType, Object data) {
|
||||
WebhookSendLog sendLog = new WebhookSendLog();
|
||||
sendLog.setTargetUrl(targetUrl);
|
||||
sendLog.setEventType(eventType);
|
||||
|
||||
try {
|
||||
// 1. Payload 생성
|
||||
WebhookPayload webhookPayload = WebhookPayload.of(eventType, data);
|
||||
String payloadJson = objectMapper.writeValueAsString(webhookPayload);
|
||||
sendLog.setPayload(payloadJson);
|
||||
|
||||
// 2. Secret Key로 HMAC-SHA256 서명 생성
|
||||
String signature = generateSignature(payloadJson);
|
||||
sendLog.setSignature(signature);
|
||||
|
||||
// 3. HTTP 요청 헤더 구성
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("X-Webhook-Signature", "sha256=" + signature);
|
||||
headers.set("X-Webhook-Event", eventType);
|
||||
headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp()));
|
||||
|
||||
// 4. 발송
|
||||
sendLog.setSentAt(LocalDateTime.now());
|
||||
HttpEntity<String> request = new HttpEntity<>(payloadJson, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(targetUrl, request, String.class);
|
||||
|
||||
// 5. 성공 로그
|
||||
sendLog.setStatusCode(response.getStatusCode().value());
|
||||
sendLog.setResponseBody(response.getBody());
|
||||
sendLog.setSuccess(response.getStatusCode().is2xxSuccessful());
|
||||
|
||||
log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}",
|
||||
eventType, targetUrl, response.getStatusCode());
|
||||
|
||||
} catch (HttpClientErrorException | HttpServerErrorException e) {
|
||||
sendLog.setStatusCode(e.getStatusCode().value());
|
||||
sendLog.setResponseBody(e.getResponseBodyAsString());
|
||||
sendLog.setSuccess(false);
|
||||
sendLog.setErrorMessage(e.getMessage());
|
||||
log.error("[Webhook] HTTP 오류 - eventType: {}, url: {}, status: {}",
|
||||
eventType, targetUrl, e.getStatusCode());
|
||||
|
||||
} catch (Exception e) {
|
||||
sendLog.setSuccess(false);
|
||||
sendLog.setErrorMessage(e.getMessage());
|
||||
log.error("[Webhook] 발송 실패 - eventType: {}, url: {}, error: {}",
|
||||
eventType, targetUrl, e.getMessage());
|
||||
}
|
||||
|
||||
return sendLogRepository.save(sendLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패 건 재시도
|
||||
*/
|
||||
@Transactional
|
||||
public void retryFailedWebhooks() {
|
||||
List<WebhookSendLog> failedLogs =
|
||||
sendLogRepository.findFailedLogs(MAX_RETRY);
|
||||
|
||||
for (WebhookSendLog failedLog : failedLogs) {
|
||||
try {
|
||||
log.info("[Webhook] 재시도 - id: {}, retry: {}", failedLog.getId(), failedLog.getRetryCount());
|
||||
failedLog.setRetryCount(failedLog.getRetryCount() + 1);
|
||||
sendLogRepository.save(failedLog);
|
||||
|
||||
send(failedLog.getTargetUrl(), failedLog.getEventType(),
|
||||
objectMapper.readValue(failedLog.getPayload(), Object.class));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[Webhook] 재시도 실패 - id: {}", failedLog.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HMAC-SHA256 서명 생성
|
||||
* Java 17 미만은 HexFormat 미지원이므로 직접 변환
|
||||
*/
|
||||
private String generateSignature(String payload) throws Exception {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(
|
||||
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
||||
mac.init(keySpec);
|
||||
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hash);
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package com.eactive.eai.rms.onl.manage.crypto;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.propertyeditors.CustomNumberEditor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.SortDefault;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import com.eactive.eai.agent.command.CommandResult;
|
||||
import com.eactive.eai.agent.command.CommonCommand;
|
||||
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
|
||||
import com.eactive.eai.rms.common.login.SessionManager;
|
||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||
|
||||
@Controller
|
||||
public class CryptoModuleConfigManController extends OnlBaseAnnotationController {
|
||||
|
||||
private static final String RELOAD_CRYPTO_MODULE_COMMAND = "com.eactive.eai.agent.security.ReloadCryptoModuleCommand";
|
||||
|
||||
private final CryptoModuleConfigManService service;
|
||||
|
||||
@Autowired
|
||||
public CryptoModuleConfigManController(CryptoModuleConfigManService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@InitBinder
|
||||
public void initBinder(WebDataBinder binder) {
|
||||
binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, true));
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/admin/security/cryptoModuleMan.view")
|
||||
public String viewList() {
|
||||
return "/onl/admin/security/cryptoModuleMan";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/admin/security/cryptoModuleMan.view", params = "cmd=DETAIL")
|
||||
public String detailView() {
|
||||
return "/onl/admin/security/cryptoModuleManDetail";
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=LIST")
|
||||
public ResponseEntity<GridResponse<CryptoModuleConfigUI>> selectList(
|
||||
@SortDefault("cryptoName") Pageable pageable,
|
||||
String searchName, String algType, String keySourceType, String useYn) {
|
||||
Page<CryptoModuleConfigUI> page = service.selectList(pageable, searchName, algType, keySourceType, useYn);
|
||||
return ResponseEntity.ok(new GridResponse<>(page));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=DETAIL")
|
||||
public ResponseEntity<CryptoModuleConfigUI> selectDetail(String cryptoId) {
|
||||
return ResponseEntity.ok(service.selectDetail(cryptoId));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=INSERT")
|
||||
public ResponseEntity<Map<String, String>> insert(@Valid CryptoModuleConfigUI ui, BindingResult bindingResult,
|
||||
HttpServletRequest request) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
for (FieldError error : bindingResult.getFieldErrors()) {
|
||||
errors.put(error.getField(), error.getDefaultMessage());
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
|
||||
}
|
||||
String cryptoId = service.insert(ui, SessionManager.getUserId(request));
|
||||
CommandResult broadcastResult = CommonCommand.builder()
|
||||
.name(RELOAD_CRYPTO_MODULE_COMMAND)
|
||||
.args(cryptoId)
|
||||
.build()
|
||||
.broadcast(agentUtilService);
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("broadcastResult", broadcastResult.getMessage(true));
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=UPDATE")
|
||||
public ResponseEntity<Map<String, String>> update(@Valid CryptoModuleConfigUI ui, BindingResult bindingResult,
|
||||
HttpServletRequest request) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
for (FieldError error : bindingResult.getFieldErrors()) {
|
||||
errors.put(error.getField(), error.getDefaultMessage());
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
|
||||
}
|
||||
service.update(ui, SessionManager.getUserId(request));
|
||||
CommandResult broadcastResult = CommonCommand.builder()
|
||||
.name(RELOAD_CRYPTO_MODULE_COMMAND)
|
||||
.args(ui.getCryptoId())
|
||||
.build()
|
||||
.broadcast(agentUtilService);
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("broadcastResult", broadcastResult.getMessage(true));
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=DELETE")
|
||||
public ResponseEntity<Map<String, String>> delete(String cryptoId) {
|
||||
service.delete(cryptoId);
|
||||
CommandResult broadcastResult = CommonCommand.builder()
|
||||
.name(RELOAD_CRYPTO_MODULE_COMMAND)
|
||||
.args(cryptoId)
|
||||
.build()
|
||||
.broadcast(agentUtilService);
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("broadcastResult", broadcastResult.getMessage(true));
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.eactive.eai.rms.onl.manage.crypto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
import com.eactive.eai.rms.common.base.BaseService;
|
||||
import com.eactive.eai.rms.data.entity.onl.security.CryptoModuleConfigDataService;
|
||||
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class CryptoModuleConfigManService extends BaseService {
|
||||
|
||||
private final CryptoModuleConfigDataService dataService;
|
||||
private final CryptoModuleConfigUiMapper mapper;
|
||||
|
||||
@Autowired
|
||||
public CryptoModuleConfigManService(CryptoModuleConfigDataService dataService,
|
||||
CryptoModuleConfigUiMapper mapper) {
|
||||
this.dataService = dataService;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public Page<CryptoModuleConfigUI> selectList(Pageable pageable, String searchName,
|
||||
String algType, String keySourceType, String useYn) {
|
||||
Page<CryptoModuleConfig> page = dataService.findAll(pageable, searchName, algType, keySourceType, useYn);
|
||||
return page.map(mapper::toVo);
|
||||
}
|
||||
|
||||
public CryptoModuleConfigUI selectDetail(String cryptoId) {
|
||||
CryptoModuleConfig entity = dataService.findById(cryptoId)
|
||||
.orElseThrow(() -> new BizException("암호화 모듈을 찾을 수 없습니다: " + cryptoId));
|
||||
return mapper.toVo(entity);
|
||||
}
|
||||
|
||||
public String insert(CryptoModuleConfigUI ui, String loginId) {
|
||||
CryptoModuleConfig entity = mapper.toEntity(ui);
|
||||
entity.setCryptoId(null);
|
||||
entity.setModifiedBy(loginId);
|
||||
entity.setModifiedAt(LocalDateTime.now());
|
||||
dataService.save(entity);
|
||||
return entity.getCryptoId();
|
||||
}
|
||||
|
||||
public void update(CryptoModuleConfigUI ui, String loginId) {
|
||||
CryptoModuleConfig entity = dataService.findById(ui.getCryptoId())
|
||||
.orElseThrow(() -> new BizException("암호화 모듈을 찾을 수 없습니다: " + ui.getCryptoId()));
|
||||
mapper.updateToEntity(ui, entity);
|
||||
entity.setModifiedBy(loginId);
|
||||
entity.setModifiedAt(LocalDateTime.now());
|
||||
dataService.save(entity);
|
||||
}
|
||||
|
||||
public void delete(String cryptoId) {
|
||||
dataService.deleteById(cryptoId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.eactive.eai.rms.onl.manage.crypto;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CryptoModuleConfigUI {
|
||||
|
||||
@JsonProperty("CRYPTO_ID")
|
||||
private String cryptoId;
|
||||
|
||||
@JsonProperty("CRYPTO_NAME")
|
||||
@NotNull
|
||||
@Size(max = 100)
|
||||
private String cryptoName;
|
||||
|
||||
@JsonProperty("CRYPTO_DESC")
|
||||
private String cryptoDesc;
|
||||
|
||||
@JsonProperty("ALG_TYPE")
|
||||
@NotNull
|
||||
private String algType;
|
||||
|
||||
@JsonProperty("CIPHER_MODE")
|
||||
@NotNull
|
||||
private String cipherMode;
|
||||
|
||||
@JsonProperty("PADDING")
|
||||
private String padding;
|
||||
|
||||
@JsonProperty("IV_HEX")
|
||||
private String ivHex;
|
||||
|
||||
@JsonProperty("KEY_SOURCE_TYPE")
|
||||
@NotNull
|
||||
private String keySourceType;
|
||||
|
||||
@JsonProperty("ENC_KEY_HEX")
|
||||
private String encKeyHex;
|
||||
|
||||
@JsonProperty("DEC_KEY_HEX")
|
||||
private String decKeyHex;
|
||||
|
||||
@JsonProperty("KEY_DERIV_STRATEGY")
|
||||
private String keyDerivStrategy;
|
||||
|
||||
@JsonProperty("KEY_DERIV_PARAMS")
|
||||
private String keyDerivParams;
|
||||
|
||||
@JsonProperty("CACHE_YN")
|
||||
@NotNull
|
||||
private String cacheYn;
|
||||
|
||||
@JsonProperty("CACHE_TTL_SEC")
|
||||
private Integer cacheTtlSec;
|
||||
|
||||
@JsonProperty("USE_YN")
|
||||
@NotNull
|
||||
private String useYn;
|
||||
|
||||
@JsonProperty("MODIFIED_BY")
|
||||
private String modifiedBy;
|
||||
|
||||
@JsonProperty("MODIFIED_AT")
|
||||
private String modifiedAt;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.eactive.eai.rms.onl.manage.crypto;
|
||||
|
||||
import org.mapstruct.BeanMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.NullValuePropertyMappingStrategy;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
import com.eactive.eai.data.mapper.GenericMapper;
|
||||
|
||||
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface CryptoModuleConfigUiMapper extends GenericMapper<CryptoModuleConfigUI, CryptoModuleConfig> {
|
||||
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void updateToEntity(CryptoModuleConfigUI ui, @MappingTarget CryptoModuleConfig entity);
|
||||
|
||||
}
|
||||
@@ -249,7 +249,12 @@ public class ApiInterfaceService extends OnlBaseService {
|
||||
StdMessageUI stdMessageUI = stdMessageUIMapper.toVo(standardMessageInfo);
|
||||
|
||||
String stdKey = stdMessageUI.getBzwkSvcKeyName();
|
||||
String inboundRestPath = StringUtils.substringAfter(stdKey, STEMSG_SERVICE_URL_DELIMITER);
|
||||
String inboundRestPath;
|
||||
if (StringUtils.contains(stdKey, STEMSG_SERVICE_HEADER_DELIMITER)) {
|
||||
inboundRestPath = StringUtils.substringBetween(stdKey, STEMSG_SERVICE_URL_DELIMITER, STEMSG_SERVICE_HEADER_DELIMITER);
|
||||
} else {
|
||||
inboundRestPath = StringUtils.substringAfter(stdKey, STEMSG_SERVICE_URL_DELIMITER);
|
||||
}
|
||||
apiInterfaceUI.setInboundRestPath(inboundRestPath);
|
||||
apiInterfaceUI.setApiFullPath(standardMessageInfo.getApifullpath());
|
||||
|
||||
@@ -257,7 +262,11 @@ public class ApiInterfaceService extends OnlBaseService {
|
||||
|
||||
String inboundHttpMethod;
|
||||
if (StringUtils.contains(stdKey, STEMSG_SERVICE_HEADER_DELIMITER)) {
|
||||
inboundHttpMethod = StringUtils.substringBetween(stdKey, STEMSG_METHOD_DELIMITER, STEMSG_SERVICE_HEADER_DELIMITER);
|
||||
if (StringUtils.contains(stdKey, STEMSG_SERVICE_URL_DELIMITER)) {
|
||||
inboundHttpMethod = StringUtils.substringBetween(stdKey, STEMSG_METHOD_DELIMITER, STEMSG_SERVICE_URL_DELIMITER);
|
||||
} else {
|
||||
inboundHttpMethod = StringUtils.substringBetween(stdKey, STEMSG_METHOD_DELIMITER, STEMSG_SERVICE_HEADER_DELIMITER);
|
||||
}
|
||||
String headerRoutingValueString = StringUtils.substringAfter(stdKey, STEMSG_SERVICE_HEADER_DELIMITER);
|
||||
|
||||
List<String> headerValues = new ArrayList<>();
|
||||
|
||||
+5
@@ -36,6 +36,7 @@ public interface ApiInterfaceUIMapper {
|
||||
@Mapping(source = "lastModifiedDate", target = "eailastamndyms")
|
||||
@Mapping(source = "apiEnabledYn", target = "apienabledyn")
|
||||
@Mapping(source = "authType", target = "authtype")
|
||||
@Mapping(source = "verinfo", target = "verinfo")
|
||||
@Mapping(target = "svchmseonot", constant = "0")
|
||||
@Mapping(target = "svcmotivusedstcd", constant = "SYNC")
|
||||
@Mapping(target = "svcprcesdsticname", constant = "SINGLE")
|
||||
@@ -57,6 +58,7 @@ public interface ApiInterfaceUIMapper {
|
||||
@Mapping(source = "eaiMessageEntity.authtype", target = "authType")
|
||||
@Mapping(target = "lastModifiedDate", source = "eaiMessageEntity.eailastamndyms")
|
||||
@Mapping(target = "svcLogLvelNo", source = "eaiMessageEntity.svcloglvelno")
|
||||
@Mapping(target = "verinfo", source = "eaiMessageEntity.verinfo")
|
||||
ApiInterfaceUI toVo(EAIMessageEntity eaiMessageEntity, List<ServiceMessageEntity> serviceMessageEntities);
|
||||
|
||||
@InheritConfiguration(name="toEaiMessageEntity")
|
||||
@@ -165,6 +167,9 @@ public interface ApiInterfaceUIMapper {
|
||||
String stdMessageKey = inboundAdapterGroupName + ApiInterfaceService.STEMSG_METHOD_DELIMITER + inboundMethod;
|
||||
|
||||
if(vo.getIsHeaderRouting() != null && vo.getIsHeaderRouting() && headerValues != null && headerValues.size() > 0){
|
||||
if(!StringUtils.isBlank(inboundRestPath)) {
|
||||
stdMessageKey += ApiInterfaceService.STEMSG_SERVICE_URL_DELIMITER + inboundRestPath;
|
||||
}
|
||||
stdMessageKey += ApiInterfaceService.STEMSG_SERVICE_HEADER_DELIMITER + String.join(ApiInterfaceService.STEMSG_HEADER_SEPARATOR, headerValues);
|
||||
}else{
|
||||
if(StringUtils.isBlank(inboundRestPath)) {
|
||||
|
||||
+7
-1
@@ -2,6 +2,10 @@ package com.eactive.eai.rms.onl.transaction.apim.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.stdmessage.StandardMessageInfo;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
|
||||
@@ -9,7 +13,9 @@ import com.eactive.eai.data.jpa.BaseRepository;
|
||||
public interface StandardMessageInfoRepositoryForApi extends BaseRepository<StandardMessageInfo, String> {
|
||||
Optional<StandardMessageInfo> findByEaisvcname(String eaiSvcName);
|
||||
|
||||
void deleteByEaisvcname(String eaiSvcName);
|
||||
@Modifying
|
||||
@Query("DELETE FROM StandardMessageInfo s WHERE s.eaisvcname = :eaiSvcName")
|
||||
void deleteByEaisvcname(@Param("eaiSvcName") String eaiSvcName);
|
||||
|
||||
boolean existsByApifullpathAndEaisvcnameNot(String fullPath, String eaiServiceName);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ public class ApiInterfaceUI {
|
||||
private Integer svcLogLvelNo;
|
||||
private String apiEnabledYn;
|
||||
private String authType;
|
||||
private String verinfo;
|
||||
private Boolean isHeaderRouting;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
|
||||
<!-- <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />-->
|
||||
|
||||
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
|
||||
<property name="PREFIX" value="${inst.Type:-ems}" />
|
||||
@@ -114,19 +114,9 @@
|
||||
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||
|
||||
<logger name="org.springframework" level="INFO" />
|
||||
<logger name="org.hibernate" level="INFO" additivity="false">
|
||||
<logger name="org.hibernate" level="INFO" >
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</logger>
|
||||
<!-- Hibernate SQL 쿼리 출력 -->
|
||||
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</logger>
|
||||
<!-- Hibernate 바인딩 파라미터 출력 -->
|
||||
<logger name="org.hibernate.type.descriptor.sql" level="TRACE" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</logger>
|
||||
<logger name="net.sf.ehcache" level="INFO" />
|
||||
<logger name="org.quartz" level="INFO" />
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# 작업내역 — feature/crypto-module (eapim-admin)
|
||||
|
||||
> 작업일: 2026-05-06
|
||||
> 브랜치: `feature/crypto-module`
|
||||
> 작업자: curry772
|
||||
> 프로젝트: `eapim-admin`
|
||||
|
||||
---
|
||||
|
||||
## 작업 목적
|
||||
|
||||
`eapim-online`에서 구현된 암호화모듈 프레임워크(`CryptoModuleManager`)의 설정 데이터(`crypto_module_config`)를
|
||||
관리자 화면에서 CRUD할 수 있는 관리 화면 개발.
|
||||
설정 변경 즉시 게이트웨이 엔진에 동적 반영되어야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 변경 파일 목록
|
||||
|
||||
### JPA 데이터 레이어 — `com.eactive.eai.rms.data.entity.onl.security`
|
||||
|
||||
| 파일 | 설명 |
|
||||
|---|---|
|
||||
| `CryptoModuleConfigRepository.java` | `BaseRepository` + `QuerydslPredicateExecutor` 조합 Repository |
|
||||
| `CryptoModuleConfigDataService.java` | 데이터 서비스 인터페이스. `findAll(pageable, searchName, algType, keySourceType, useYn)` |
|
||||
| `CryptoModuleConfigDataServiceImpl.java` | QueryDSL predicate 기반 검색 구현. `QCryptoModuleConfig` 사용 |
|
||||
|
||||
### 관리 화면 레이어 — `com.eactive.eai.rms.onl.manage.crypto`
|
||||
|
||||
| 파일 | 설명 |
|
||||
|---|---|
|
||||
| `CryptoModuleConfigUI.java` | UI VO (`@Data`, `@JsonProperty` 대문자+언더스코어 규칙). `@NotNull` 검증 포함 |
|
||||
| `CryptoModuleConfigUiMapper.java` | MapStruct Entity↔UI 매퍼. `updateToEntity` (null 무시) 포함 |
|
||||
| `CryptoModuleConfigManService.java` | CRUD 비즈니스 서비스. insert/update 시 `modifiedBy`·`modifiedAt` 자동 설정 |
|
||||
| `CryptoModuleConfigManController.java` | Spring MVC Controller. INSERT/UPDATE/DELETE 후 엔진 재로드 커맨드 broadcast |
|
||||
|
||||
### JSP 화면 — `WebContent/jsp/onl/admin/security/`
|
||||
|
||||
| 파일 | 설명 |
|
||||
|---|---|
|
||||
| `cryptoModuleMan.jsp` | 목록 화면. jqGrid, 4가지 검색 조건(모듈명·알고리즘·키소스·사용여부) |
|
||||
| `cryptoModuleManDetail.jsp` | 상세/등록/수정 화면. `keySourceType` 선택에 따라 STATIC/DYNAMIC 섹션 토글 |
|
||||
|
||||
---
|
||||
|
||||
## URL 구조
|
||||
|
||||
| 용도 | URL |
|
||||
|---|---|
|
||||
| 목록 View | `GET /onl/admin/security/cryptoModuleMan.view` |
|
||||
| 상세 View | `GET /onl/admin/security/cryptoModuleMan.view?cmd=DETAIL&cryptoId={id}` |
|
||||
| 목록 조회 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=LIST` |
|
||||
| 상세 조회 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=DETAIL` |
|
||||
| 등록 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=INSERT` |
|
||||
| 수정 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=UPDATE` |
|
||||
| 삭제 | `POST /onl/admin/security/cryptoModuleMan.json?cmd=DELETE` |
|
||||
|
||||
---
|
||||
|
||||
## 엔진 동적 반영 구조
|
||||
|
||||
```
|
||||
Controller (INSERT / UPDATE / DELETE)
|
||||
└─ service.insert() / update() / delete() ← DB 반영
|
||||
└─ CommonCommand.builder()
|
||||
.name("com.eactive.eai.agent.security.ReloadCryptoModuleCommand")
|
||||
.args(cryptoId)
|
||||
.build()
|
||||
.broadcast(agentUtilService) ← 게이트웨이 전 인스턴스 broadcast
|
||||
└─ CryptoModuleManager.reload(cryptoId)
|
||||
├─ configMap 에서 cryptoId 항목 제거
|
||||
├─ dynamicKeyCache 에서 관련 항목 제거
|
||||
└─ DB 재조회 → useYn=Y 이면 configMap 재등록
|
||||
(DELETE 후 호출 시: DB에 없으므로 제거만 수행)
|
||||
```
|
||||
|
||||
- `RELOAD_CRYPTO_MODULE_COMMAND` 상수는 `CommonCommand.java`에 추가하지 않고
|
||||
`CryptoModuleConfigManController` 내부에 `private static final String`으로 선언
|
||||
|
||||
---
|
||||
|
||||
## 화면 설계
|
||||
|
||||
### 목록 화면 (`cryptoModuleMan.jsp`)
|
||||
|
||||
**검색 조건**
|
||||
|
||||
| 항목 | 유형 | 비고 |
|
||||
|---|---|---|
|
||||
| 모듈명 | text | 부분 일치 (containsIgnoreCase) |
|
||||
| 알고리즘 | select | 전체 / AES / ARIA |
|
||||
| 키 소스 | select | 전체 / STATIC / DYNAMIC |
|
||||
| 사용 여부 | select | 전체 / Y / N |
|
||||
|
||||
**그리드 컬럼**: 모듈명 · 설명 · 알고리즘 · 운영모드 · 키 소스 · 사용여부
|
||||
(CRYPTO_ID는 hidden 컬럼으로 포함, 더블클릭 시 상세 화면 이동에 사용)
|
||||
|
||||
### 상세 화면 (`cryptoModuleManDetail.jsp`)
|
||||
|
||||
**공통 필드**: 모듈명\* · 설명 · 알고리즘\* · 운영모드\* · 패딩 · IV(Hex) · 키소스유형\* · 동적키캐시\* · 캐시TTL · 사용여부\*
|
||||
|
||||
**STATIC 섹션** (`keySourceType = STATIC` 일 때만 표시):
|
||||
- 암호화 키(Hex) \*
|
||||
- 복호화 키(Hex) (미입력 시 암호화 키와 동일)
|
||||
|
||||
**DYNAMIC 섹션** (`keySourceType = DYNAMIC` 일 때만 표시):
|
||||
- 키 도출 전략 FQCN \*
|
||||
- 키 도출 파라미터 JSON \*
|
||||
|
||||
`keySourceType` select 변경 시 JavaScript `toggleKeySourceFields()`로 즉시 토글.
|
||||
|
||||
---
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
### 1. `@InitBinder` — Integer 빈 문자열 처리
|
||||
`cacheTtlSec`(Integer)에 빈 문자열이 전송될 수 있으므로 Controller에 `@InitBinder` 등록:
|
||||
```java
|
||||
binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, true));
|
||||
```
|
||||
|
||||
### 2. 로그인 사용자 — `SessionManager.getUserId(request)`
|
||||
`HttpServletRequest`를 Controller에서 받아 `SessionManager.getUserId(request)`로 추출,
|
||||
Service의 `insert()` / `update()` 에 `loginId`로 전달 → `modifiedBy` 설정.
|
||||
|
||||
### 3. JSON 필드명 규칙
|
||||
`@JsonProperty`는 DB 컬럼명 기반 대문자+언더스코어 (`CRYPTO_ID`, `KEY_SOURCE_TYPE` 등).
|
||||
JSP `detail()` 함수에서 제네릭 루프 대신 명시적 필드 매핑으로 처리
|
||||
(기존 `name.toUpperCase()` 패턴은 언더스코어 있는 컬럼명과 불일치하므로).
|
||||
|
||||
### 4. DELETE 후 엔진 반영
|
||||
`CryptoModuleManager.reload(cryptoId)` — DB에서 삭제된 후 호출하면
|
||||
`loader.findById(cryptoId)` 가 empty를 반환 → `configMap` 에서 제거만 수행.
|
||||
별도 RemoveCommand 없이 동일한 `ReloadCryptoModuleCommand` 재사용.
|
||||
|
||||
---
|
||||
|
||||
## 미완료 / 다음 작업
|
||||
|
||||
- [ ] `kjb-gradle.sh compileJava` 실행 → `QCryptoModuleConfig` Q-class 생성 확인
|
||||
- [ ] 관리자 메뉴 테이블에 경로 등록 (`/onl/admin/security/cryptoModuleMan.view`)
|
||||
- [ ] 화면 접근 권한(ACL) 등록
|
||||
- [ ] 전체 파일 커밋 (eapim-admin)
|
||||
@@ -0,0 +1,149 @@
|
||||
# 작업내역 — feature/crypto-module (eapim-admin)
|
||||
|
||||
> 작업일: 2026-05-07
|
||||
> 브랜치: `feature/crypto-module`
|
||||
> 작업자: curry772
|
||||
> 프로젝트: `eapim-admin`
|
||||
|
||||
---
|
||||
|
||||
## 작업 목적
|
||||
|
||||
2026-05-06 작업 미완료 항목 처리 및 기능 테스트 중 발견된 버그 수정, UX 개선.
|
||||
|
||||
---
|
||||
|
||||
## 변경 파일 목록
|
||||
|
||||
### DB DDL
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| `EMSADM.CRYPTO_MODULE_CONFIG` 테이블 컬럼 추가 | 기존 DDL에서 누락된 `MODIFIED_BY`, `MODIFIED_AT` 컬럼 추가 |
|
||||
|
||||
```sql
|
||||
ALTER TABLE EMSADM.CRYPTO_MODULE_CONFIG
|
||||
ADD (
|
||||
MODIFIED_BY VARCHAR2(50),
|
||||
MODIFIED_AT TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
> 전체 DDL (신규 환경용)
|
||||
```sql
|
||||
CREATE TABLE EMSADM.CRYPTO_MODULE_CONFIG (
|
||||
CRYPTO_ID VARCHAR2(36) NOT NULL,
|
||||
CRYPTO_NAME VARCHAR2(100) NOT NULL,
|
||||
CRYPTO_DESC VARCHAR2(500),
|
||||
ALG_TYPE VARCHAR2(10) NOT NULL,
|
||||
CIPHER_MODE VARCHAR2(20) NOT NULL,
|
||||
PADDING VARCHAR2(30),
|
||||
IV_HEX VARCHAR2(200),
|
||||
KEY_SOURCE_TYPE VARCHAR2(20) NOT NULL,
|
||||
ENC_KEY_HEX VARCHAR2(200),
|
||||
DEC_KEY_HEX VARCHAR2(200),
|
||||
KEY_DERIV_STRATEGY VARCHAR2(200),
|
||||
KEY_DERIV_PARAMS VARCHAR2(2000),
|
||||
CACHE_YN CHAR(1) NOT NULL,
|
||||
CACHE_TTL_SEC NUMBER(10),
|
||||
USE_YN CHAR(1) NOT NULL,
|
||||
MODIFIED_BY VARCHAR2(50),
|
||||
MODIFIED_AT TIMESTAMP,
|
||||
CONSTRAINT PK_CRYPTO_MODULE_CONFIG PRIMARY KEY (CRYPTO_ID),
|
||||
CONSTRAINT UQ_CRYPTO_MODULE_CONFIG_NAME UNIQUE (CRYPTO_NAME)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 비즈니스 레이어 — `CryptoModuleConfigManService.java`
|
||||
|
||||
#### 버그 수정: detached entity passed to persist
|
||||
|
||||
**원인**
|
||||
- `AbstractEntity`가 `Persistable<I>`를 구현하고 `@Transient boolean isNew = true`로 선언
|
||||
- `@PostLoad`에서 `isNew = false`로 전환하는 구조
|
||||
- `AbstractDataService.getById(id)`는 `repository.getReferenceById(id)` (lazy proxy) 반환
|
||||
- lazy proxy는 실제 DB 조회를 하지 않아 `@PostLoad`가 호출되지 않음 → `isNew = true` 유지
|
||||
- `save(proxy)` 호출 시 `isNew() = true` → `em.persist(proxy)` → detached entity 에러
|
||||
|
||||
**수정 내용**
|
||||
|
||||
| 메서드 | 변경 전 | 변경 후 |
|
||||
|---|---|---|
|
||||
| `selectDetail()` | `dataService.getById(cryptoId)` | `dataService.findById(cryptoId).orElseThrow(...)` |
|
||||
| `update()` | `dataService.getById(ui.getCryptoId())` | `dataService.findById(ui.getCryptoId()).orElseThrow(...)` |
|
||||
| `insert()` | — | `entity.setCryptoId(null)` 추가 |
|
||||
|
||||
- `findById()`는 실제 DB 조회 → `@PostLoad` 호출 → `isNew = false` → `merge()` 정상 동작
|
||||
- `insert()` 에서 `setCryptoId(null)` 추가: JSP `<input type="hidden" name="cryptoId" />`가 빈 문자열 `""`로 전송될 때 UUID 생성기가 정상 동작하도록 보장
|
||||
|
||||
---
|
||||
|
||||
### 목록 화면 — `cryptoModuleMan.jsp`
|
||||
|
||||
#### 버그 수정: 검색 조건 누락
|
||||
|
||||
**원인**
|
||||
- 공통 함수 `getSearchForJqgrid()`는 `name^=search` 패턴 필드만 자동 수집
|
||||
- `algType`, `keySourceType`, `useYn`은 해당 패턴이 아니어서 검색 버튼 클릭 시 postData에서 누락
|
||||
|
||||
**수정 내용**
|
||||
- 검색 버튼 클릭 핸들러에서 세 필드를 명시적으로 postData에 추가
|
||||
|
||||
```javascript
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
postData['algType'] = $('select[name=algType]').val();
|
||||
postData['keySourceType'] = $('select[name=keySourceType]').val();
|
||||
postData['useYn'] = $('select[name=useYn]').val();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 상세 화면 — `cryptoModuleManDetail.jsp`
|
||||
|
||||
#### 기능 추가 1: 텍스트 → HEX 변환 보조 입력 + 키 길이 표시
|
||||
|
||||
**배경**
|
||||
- AES/ARIA 암호화 키, 복호화 키, IV를 HEX 값으로 직접 입력하는 것은 불편
|
||||
- 단, HEX → 텍스트 역변환이 항상 가능하지 않음 (랜덤 바이트 키는 유효하지 않은 UTF-8 시퀀스 포함 가능)
|
||||
- 텍스트 변환 기능은 개발/테스트 편의용, 운영 환경에서는 랜덤 키 생성 권장
|
||||
|
||||
**추가 내용**
|
||||
- `encKeyHex`, `decKeyHex`, `ivHex` 각 필드에 텍스트 입력 보조 필드 추가
|
||||
- 텍스트 입력 시 `TextEncoder`(UTF-8)로 HEX 자동 변환 → HEX 필드에 반영
|
||||
- 보조 입력 필드는 `name` 속성 없음 → form submit에 포함되지 않음
|
||||
- HEX 필드 옆에 키 길이 실시간 표시 (`N bytes (M자)` 형식)
|
||||
- HEX 직접 입력 시 및 조회 시에도 즉시 반영
|
||||
- 참고 기준: AES-128: 16bytes / AES-192: 24bytes / AES-256: 32bytes
|
||||
|
||||
**추가 함수**
|
||||
```javascript
|
||||
textToHex(text) // TextEncoder UTF-8 기반 HEX 변환
|
||||
updateHexFromText(...) // 텍스트 입력 → HEX 필드 갱신 + 길이 표시
|
||||
updateHexLength(hex, id) // HEX 문자열 길이 계산 및 표시
|
||||
```
|
||||
|
||||
#### 기능 추가 2: 수정자 / 수정일시 정보 표시
|
||||
|
||||
- 테이블 하단에 수정자/수정일시 행 추가 (read-only `<span>` 표시)
|
||||
- 신규 등록 화면: 행 숨김 (`display:none`)
|
||||
- 상세 조회 화면: 값이 있을 때만 행 표시
|
||||
- `MODIFIED_AT` 포맷: `LocalDateTime.toString()` 결과의 `T` → 공백 치환 (`2026-05-07 09:30:00`)
|
||||
|
||||
---
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
### `getById()` vs `findById()`
|
||||
이 프로젝트에서 `AbstractDataService.getById()`는 `getReferenceById()`를 래핑한다.
|
||||
`AbstractEntity`가 `Persistable<isNew>`를 구현하므로, lazy proxy 상태에서는 `isNew = true`가 유지되어 `save()` 시 `persist()`가 호출되는 문제가 있다.
|
||||
**수정/조회 후 저장이 필요한 경우 반드시 `findById()`를 사용해야 한다.**
|
||||
|
||||
---
|
||||
|
||||
## 미완료 / 다음 작업
|
||||
|
||||
- [ ] 관리자 메뉴 테이블에 경로 등록 (`/onl/admin/security/cryptoModuleMan.view`)
|
||||
- [ ] 화면 접근 권한(ACL) 등록
|
||||
- [ ] 전체 파일 커밋 (eapim-admin + eapim-online 양쪽)
|
||||
Reference in New Issue
Block a user