Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/main/java/com/eactive/eai/rms/onl/apim/approval/PortalApprovalManController.java
This commit is contained in:
Rinjae
2026-06-12 16:08:41 +09:00
66 changed files with 2817 additions and 761 deletions
@@ -69,6 +69,7 @@
<context:component-scan base-package="com.eactive.apim.portal" /> <context:component-scan base-package="com.eactive.apim.portal" />
<context:component-scan base-package="com.eactive.ext.kjb.statistics" /> <context:component-scan base-package="com.eactive.ext.kjb.statistics" />
<context:component-scan base-package="com.eactive.ext.djb" />
<bean id="cacheManager" <bean id="cacheManager"
+8
View File
@@ -36,6 +36,14 @@
// save the returnValue in options so that it is available in the callback function // save the returnValue in options so that it is available in the callback function
optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.returnValue = $frame[0].contentWindow.window.returnValue;
optns.onClose(); optns.onClose();
// Remove iframe from DOM so window.frames doesn't accumulate stale entries;
// without this, a second showModal call leaves frames[0] pointing to the first
// (closed) iframe, causing the previous returnValue to bleed into the next callback.
var $dialogEl = $frame.closest('.ui-dialog');
setTimeout(function() {
$frame.remove();
if ($dialogEl.length) $dialogEl.remove();
}, 0);
if(optns.option && optns.option.fullScreenPopup){ if(optns.option && optns.option.fullScreenPopup){
$('iframe[name=topFrame]',window.parent.document).css('display', 'block'); $('iframe[name=topFrame]',window.parent.document).css('display', 'block');
+4
View File
@@ -30,6 +30,10 @@
<script language="javascript" src="<c:url value="/js/jquery.contextMenu.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.contextMenu.js"/>"></script>
<script language="javascript" src="<c:url value="/js/custom.js"/>"></script> <script language="javascript" src="<c:url value="/js/custom.js"/>"></script>
<script language="javascript" src="<c:url value="/js/common-${locale}.js"/>"></script> <script language="javascript" src="<c:url value="/js/common-${locale}.js"/>"></script>
<script type="text/javascript">
/* 자동 로그아웃: iframe 내 활동을 sessionStorage에 기록 (main.jsp 타이머와 공유) */
(function() { var m = function() { sessionStorage.setItem('lastActivity', String(Date.now())); }; ['mousemove','keydown','click','touchstart'].forEach(function(e){ document.addEventListener(e, m, true); }); })();
</script>
<script language="javascript" src="<c:url value="/js/jquery.inputmask.bundle.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.inputmask.bundle.min.js"/>"></script>
<script language="javascript" src="<c:url value="/js/jquery.searchabledropdown-1.0.8.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.searchabledropdown-1.0.8.min.js"/>"></script>
<%-- <script language="javascript" src="<c:url value="/js/jquery.searchable-1.1.0.min.js"/>"></script> --%> <%-- <script language="javascript" src="<c:url value="/js/jquery.searchable-1.1.0.min.js"/>"></script> --%>
+87
View File
@@ -99,6 +99,93 @@
<iframe class="topMenu" src="" id="topFrame" name="topFrame" scrolling="no" noresize="noresize"></iframe> <iframe class="topMenu" src="" id="topFrame" name="topFrame" scrolling="no" noresize="noresize"></iframe>
<iframe class="leftMenu" src="" name="leftFrame" scrolling="no" noresize="noresize"></iframe> <iframe class="leftMenu" src="" name="leftFrame" scrolling="no" noresize="noresize"></iframe>
<iframe class="rightCon" src="" name="mainFrame" scrolling="auto"></iframe> <iframe class="rightCon" src="" name="mainFrame" scrolling="auto"></iframe>
<%-- 자동 로그아웃 경고 모달 --%>
<div id="autoLogoutModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:99999;">
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; padding:30px 40px; border-radius:4px; text-align:center; min-width:320px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<h3 style="margin:0 0 12px; color:#333; font-size:16px;">자동 로그아웃 안내</h3>
<p style="margin:0 0 22px; color:#666; font-size:14px;">
장시간 사용하지 않아 <strong><span id="autoLogoutSeconds">60</span>초</strong> 후 자동 로그아웃됩니다.
</p>
<button id="autoLogoutContinue" style="margin-right:8px; padding:8px 22px; background:#337ab7; color:#fff; border:none; border-radius:3px; cursor:pointer; font-size:13px;">계속 사용</button>
<button id="autoLogoutNow" style="padding:8px 22px; background:#d9534f; color:#fff; border:none; border-radius:3px; cursor:pointer; font-size:13px;">로그아웃</button>
</div>
</div>
<script type="text/javascript">
(function () {
var IDLE_MS = ${autoLogoutTimeout} * 60 * 1000; // 설정값(분) → ms
var WARN_MS = 60 * 1000; // 로그아웃 1분 전 경고
var CHECK_INTERVAL = 5000; // 5초 간격 체크
var countdownTimer = null;
/* sessionStorage: iframe 포함 모든 동일-origin 프레임과 공유 */
function getLastActivity() {
var t = sessionStorage.getItem('lastActivity');
return t ? parseInt(t, 10) : Date.now();
}
function markActivity() {
sessionStorage.setItem('lastActivity', String(Date.now()));
}
function resetTimer() {
markActivity();
if ($('#autoLogoutModal').is(':visible')) {
$('#autoLogoutModal').hide();
clearInterval(countdownTimer);
}
}
function showWarning(remainMs) {
var sec = Math.ceil(remainMs / 1000);
$('#autoLogoutSeconds').text(sec);
$('#autoLogoutModal').show();
clearInterval(countdownTimer);
countdownTimer = setInterval(function () {
sec--;
if (sec <= 0) {
clearInterval(countdownTimer);
doLogout();
} else {
$('#autoLogoutSeconds').text(sec);
}
}, 1000);
}
function doLogout() {
location.href = '<%=request.getContextPath()%>/rms/logout.do';
}
/* 상위 프레임(main.jsp) 활동 감지 */
['mousemove', 'keydown', 'click', 'touchstart'].forEach(function (e) {
document.addEventListener(e, markActivity, true);
});
/* 주기적 유휴 시간 체크 */
setInterval(function () {
var idle = Date.now() - getLastActivity();
var remain = IDLE_MS - idle;
if (remain <= 0) {
clearInterval(countdownTimer);
doLogout();
} else if (remain <= WARN_MS && !$('#autoLogoutModal').is(':visible')) {
showWarning(remain);
}
}, CHECK_INTERVAL);
/* 버튼 이벤트 */
$(document).on('click', '#autoLogoutContinue', function () {
resetTimer();
$.get('<%=request.getContextPath()%>/checkSession.do'); // 서버 세션도 갱신
});
$(document).on('click', '#autoLogoutNow', function () {
doLogout();
});
/* 페이지 로드 시 초기화 */
markActivity();
})();
</script>
</body> </body>
</html> </html>
+8
View File
@@ -36,6 +36,14 @@
// save the returnValue in options so that it is available in the callback function // save the returnValue in options so that it is available in the callback function
optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.returnValue = $frame[0].contentWindow.window.returnValue;
optns.onClose(); optns.onClose();
// Remove iframe from DOM so window.frames doesn't accumulate stale entries;
// without this, a second showModal call leaves frames[0] pointing to the first
// (closed) iframe, causing the previous returnValue to bleed into the next callback.
var $dialogEl = $frame.closest('.ui-dialog');
setTimeout(function() {
$frame.remove();
if ($dialogEl.length) $dialogEl.remove();
}, 0);
if(optns.option && optns.option.fullScreenPopup){ if(optns.option && optns.option.fullScreenPopup){
$('iframe[name=topFrame]',window.parent.document).css('display', 'block'); $('iframe[name=topFrame]',window.parent.document).css('display', 'block');
@@ -16,173 +16,486 @@
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" > <!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />';
var isDetail = false; var isDetail = false;
function init(url,key,callback){
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({ $.ajax({
type : "POST", type: "POST",
url:url, url: url,
dataType:"json", dataType: "json",
data:{cmd: 'LIST_INIT_COMBO'}, data: {cmd: 'LIST_INIT_COMBO'},
success:function(json){ success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if(typeof callback === 'function') { if (typeof callback === 'function') {
callback(url,key); callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function detail(url,key){
if (!isDetail)return;
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'DETAIL', name : key},
success:function(json){
var data = json;
$("input[name=name]").attr('readonly',true);
$("input[name=desc]").attr('readonly',true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){ function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name"); var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase(); var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
}); });
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', adapterId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '어댑터가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() { $(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key ="${param.name}"; var key = "${param.name}";
if (key != "" && key !="null"){ if (key != "" && key != "null") {
isDetail = true; isDetail = true;
} }
init(url,key,detail); init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function(){ $("#btn_modify").click(function() {
var postData = $('#ajaxForm').serializeArray(); $(this).blur();
if (isDetail){ var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 어댑터를 등록하시겠습니까?';
postData.push({ name: "cmd" , value:"UPDATE"}); showConfirm(confirmMsg, {
}else{ title: '저장 확인',
postData.push({ name: "cmd" , value:"INSERT"}); confirmText: '저장',
} onConfirm: function() {
$.ajax({ var postData = $('#ajaxForm').serializeArray();
type : "POST", if (isDetail) {
url:url, postData.push({name: "cmd", value: "UPDATE"});
data:postData, } else {
success:function(args){ postData.push({name: "cmd", value: "INSERT"});
alert("<%= localeMessage.getString("common.saveMsg") %>"); }
goNav(returnUrl);//LIST로 이동 var isInsert = !isDetail;
}, $.ajax({
error:function(e){ type: "POST",
alert(e.responseText); url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return; $("#btn_delete").click(function() {
$(this).blur();
var postData = $('#ajaxForm').serializeArray(); var adapterName = $('input[name=desc]').val() || $('input[name=name]').val();
postData.push({ name: "cmd" , value:"DELETE"}); showConfirm('<strong>[' + adapterName + ']</strong> 어댑터를 삭제하시겠습니까?', {
$.ajax({ type: 'error',
type : "POST", title: '삭제 확인',
url:url, confirmText: '삭제',
data:postData, onConfirm: function() {
success:function(args){ var postData = $('#ajaxForm').serializeArray();
alert("<%= localeMessage.getString("common.deleteMsg") %>"); postData.push({name: "cmd", value: "DELETE"});
goNav(returnUrl);//LIST로 이동 $.ajax({
type: "POST",
}, url: url,
error:function(e){ data: postData,
alert(e.responseText); success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_previous").click(function(){
goNav(returnUrl);//LIST로 이동 $("#btn_previous").click(function() {
goNav(returnUrl);
}); });
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
});
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</script> </script>
</head> </head>
<body> <body>
<div class="right_box"> <div class="right_box">
<div class="content_top"> <div class="content_top">
<ul class="path"> <ul class="path">
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button> <button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button> <button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button> <button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%> </div>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div>
<div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infAdpConMan.title") %></span></div>
<table id="grid" ></table> <div class="title-wrap">
<div id="pager"></div> <div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip"><%= localeMessage.getString("infAdpConMan.title") %></span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- detail --> <!-- 탭 네비게이션 -->
<form id="ajaxForm"> <div class="detail-tabs">
<table class="table_row" cellspacing="0"> <button type="button" class="detail-tab active" data-tab="config">
<tr> <i class="material-icons">settings</i> 설정
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th> </button>
<td><input type="text" name="name" /></td> <button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
</tr> <i class="material-icons">analytics</i> 현황
<tr> </button>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaDes") %></th> </div>
<td><input type="text" name="desc" /></td>
</tr> <!-- 설정 탭 -->
<tr> <div class="tab-content active" id="tabConfigContent">
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th> <form id="ajaxForm">
<td><input type="text" name="thresholdPerSecond" /></td> <span class="section-label">기본 정보</span>
</tr> <table class="form-table" cellspacing="0">
<tr> <colgroup>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th> <col style="width:150px" />
<td><input type="text" name="threshold" /></td> <col />
</tr> </colgroup>
<tr> <tbody>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th> <tr>
<td> <th><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<div class="select-style"> <td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
<select name="thresholdTimeUnit" /> </tr>
</div> <tr>
</td> <th><%= localeMessage.getString("infAdpConMan.adaDes") %></th>
</tr> <td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
<tr> </tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th> <tr>
<td> <th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<div class="select-style"> <td>
<select name="useYn" /> <select name="useYn" class="styled-select"></select>
</div> </td>
</td> </tr>
</tr> </tbody>
</table> </table>
</form>
</div><!-- end content_middle --> <span class="section-label">유량제어 설정</span>
</div><!-- end right_box --> <table class="form-table" cellspacing="0">
</body> <colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html> </html>
@@ -16,170 +16,501 @@
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" > <!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
var isDetail = false; var isDetail = false;
function init(url,key,callback){
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({ $.ajax({
type : "POST", type: "POST",
url:url, url: url,
dataType:"json", dataType: "json",
data:{cmd: 'LIST_INIT_COMBO'}, data: {cmd: 'LIST_INIT_COMBO'},
success:function(json){ success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if(typeof callback === 'function') { if (typeof callback === 'function') {
callback(url,key); callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function detail(url,key){
if (!isDetail)return;
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'DETAIL', name : key},
success:function(json){
var data = json;
$("input[name=name]").attr('readonly',true);
$("input[name=desc]").attr('readonly',true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){ function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name"); var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase(); var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
}); });
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
//버킷 현황 조회
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', clientId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
// 전체 합산 계산
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
// 요약 영역 표시
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '그룹이 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() { $(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key ="${param.name}"; var key = "${param.name}";
if (key != "" && key !="null"){ if (key != "" && key != "null") {
isDetail = true; isDetail = true;
} }
init(url,key,detail); init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function(){ $("#btn_modify").click(function() {
var postData = $('#ajaxForm').serializeArray(); $(this).blur();
if (isDetail){ var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 클라이언트를 등록하시겠습니까?';
postData.push({ name: "cmd" , value:"UPDATE"}); showConfirm(confirmMsg, {
}else{ title: '저장 확인',
postData.push({ name: "cmd" , value:"INSERT"}); confirmText: '저장',
} onConfirm: function() {
$.ajax({ var postData = $('#ajaxForm').serializeArray();
type : "POST", if (isDetail) {
url:url, postData.push({name: "cmd", value: "UPDATE"});
data:postData, } else {
success:function(args){ postData.push({name: "cmd", value: "INSERT"});
alert("<%= localeMessage.getString("common.saveMsg") %>"); }
goNav(returnUrl);//LIST로 이동 var isInsert = !isDetail;
}, $.ajax({
error:function(e){ type: "POST",
alert(e.responseText); url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return; $("#btn_delete").click(function() {
$(this).blur();
var postData = $('#ajaxForm').serializeArray(); var clientName = $('input[name=desc]').val() || $('input[name=name]').val();
postData.push({ name: "cmd" , value:"DELETE"}); showConfirm('<strong>[' + clientName + ']</strong> 클라이언트를 삭제하시겠습니까?', {
$.ajax({ type: 'error',
type : "POST", title: '삭제 확인',
url:url, confirmText: '삭제',
data:postData, onConfirm: function() {
success:function(args){ var postData = $('#ajaxForm').serializeArray();
alert("<%= localeMessage.getString("common.deleteMsg") %>"); postData.push({name: "cmd", value: "DELETE"});
goNav(returnUrl);//LIST로 이동 $.ajax({
type: "POST",
}, url: url,
error:function(e){ data: postData,
alert(e.responseText); success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_previous").click(function(){
goNav(returnUrl);//LIST로 이동 $("#btn_previous").click(function() {
goNav(returnUrl);
}); });
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
});
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
$(document).keydown(function(e) {
if (e.keyCode === 27) {
// ESC 키 처리 (필요시 모달 닫기 등)
}
});
});
</script> </script>
</head> </head>
<body> <body>
<div class="right_box"> <div class="right_box">
<div class="content_top"> <div class="content_top">
<ul class="path"> <ul class="path">
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button> <button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button> <button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button> <button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div> </div>
<div class="title">클라이언트 유량제어 <span class="tooltip" >클라이언트 유량제어 </span></div>
<table id="grid" ></table> <div class="title-wrap">
<div id="pager"></div> <div class="title">클라이언트 유량제어<span class="tooltip">클라이언트 유량제어</span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- detail --> <!-- 탭 네비게이션 -->
<form id="ajaxForm"> <div class="detail-tabs">
<table class="table_row" cellspacing="0"> <button type="button" class="detail-tab active" data-tab="config">
<tr> <i class="material-icons">settings</i> 설정
<th style="width:20%;">클라이언트 ID</th> </button>
<td><input type="text" name="name" /></td> <button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
</tr> <i class="material-icons">analytics</i> 현황
<tr> </button>
<th style="width:20%;">클라이언트명</th> </div>
<td><input type="text" name="desc" /></td>
</tr> <!-- 설정 탭 -->
<tr> <div class="tab-content active" id="tabConfigContent">
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th> <form id="ajaxForm">
<td><input type="text" name="thresholdPerSecond" /></td> <span class="section-label">기본 정보</span>
</tr> <table class="form-table" cellspacing="0">
<tr> <colgroup>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th> <col style="width:150px" />
<td><input type="text" name="threshold" /></td> <col />
</tr> </colgroup>
<tr> <tbody>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th> <tr>
<td> <th>클라이언트 ID</th>
<div class="select-style"> <td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
<select name="thresholdTimeUnit" /> </tr>
</div> <tr>
</td> <th>클라이언트명</th>
</tr> <td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
<tr> </tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th> <tr>
<td> <th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<div class="select-style"> <td>
<select name="useYn" /> <select name="useYn" class="styled-select"></select>
</div> </td>
</td> </tr>
</tr> </tbody>
</table> </table>
</form>
</div><!-- end content_middle --> <span class="section-label">유량제어 설정</span>
</div><!-- end right_box --> <table class="form-table" cellspacing="0">
</body> <colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
<div class="bucket-summary-item">
<span class="bucket-summary-label">적용 API</span>
<span class="bucket-summary-value" id="summaryActiveApi">0<span class="unit">개</span></span>
<span class="bucket-summary-sub" id="summaryActiveApiList"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html> </html>
@@ -16,173 +16,486 @@
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" > <!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />';
var isDetail = false; var isDetail = false;
function init(url,key,callback){
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({ $.ajax({
type : "POST", type: "POST",
url:url, url: url,
dataType:"json", dataType: "json",
data:{cmd: 'LIST_INIT_COMBO'}, data: {cmd: 'LIST_INIT_COMBO'},
success:function(json){ success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if(typeof callback === 'function') { if (typeof callback === 'function') {
callback(url,key); callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function detail(url,key){
if (!isDetail)return;
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'DETAIL', name : key},
success:function(json){
var data = json;
$("input[name=name]").attr('readonly',true);
$("input[name=desc]").attr('readonly',true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){ function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name"); var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase(); var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
}); });
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', interfaceId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '인터페이스가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() { $(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key ="${param.name}"; var key = "${param.name}";
if (key != "" && key !="null"){ if (key != "" && key != "null") {
isDetail = true; isDetail = true;
} }
init(url,key,detail); init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function(){ $("#btn_modify").click(function() {
var postData = $('#ajaxForm').serializeArray(); $(this).blur();
if (isDetail){ var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 인터페이스를 등록하시겠습니까?';
postData.push({ name: "cmd" , value:"UPDATE"}); showConfirm(confirmMsg, {
}else{ title: '저장 확인',
postData.push({ name: "cmd" , value:"INSERT"}); confirmText: '저장',
} onConfirm: function() {
$.ajax({ var postData = $('#ajaxForm').serializeArray();
type : "POST", if (isDetail) {
url:url, postData.push({name: "cmd", value: "UPDATE"});
data:postData, } else {
success:function(args){ postData.push({name: "cmd", value: "INSERT"});
alert("<%= localeMessage.getString("common.saveMsg") %>"); }
goNav(returnUrl);//LIST로 이동 var isInsert = !isDetail;
}, $.ajax({
error:function(e){ type: "POST",
alert(e.responseText); url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return; $("#btn_delete").click(function() {
$(this).blur();
var postData = $('#ajaxForm').serializeArray(); var ifName = $('input[name=desc]').val() || $('input[name=name]').val();
postData.push({ name: "cmd" , value:"DELETE"}); showConfirm('<strong>[' + ifName + ']</strong> 인터페이스를 삭제하시겠습니까?', {
$.ajax({ type: 'error',
type : "POST", title: '삭제 확인',
url:url, confirmText: '삭제',
data:postData, onConfirm: function() {
success:function(args){ var postData = $('#ajaxForm').serializeArray();
alert("<%= localeMessage.getString("common.deleteMsg") %>"); postData.push({name: "cmd", value: "DELETE"});
goNav(returnUrl);//LIST로 이동 $.ajax({
type: "POST",
}, url: url,
error:function(e){ data: postData,
alert(e.responseText); success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_previous").click(function(){
goNav(returnUrl);//LIST로 이동 $("#btn_previous").click(function() {
goNav(returnUrl);
}); });
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
});
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</script> </script>
</head> </head>
<body> <body>
<div class="right_box"> <div class="right_box">
<div class="content_top"> <div class="content_top">
<ul class="path"> <ul class="path">
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button> <button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button> <button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button> <button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%> </div>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div>
<div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infIfConMan.title") %></span></div>
<table id="grid" ></table> <div class="title-wrap">
<div id="pager"></div> <div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip"><%= localeMessage.getString("infIfConMan.title") %></span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- detail --> <!-- 탭 네비게이션 -->
<form id="ajaxForm"> <div class="detail-tabs">
<table class="table_row" cellspacing="0"> <button type="button" class="detail-tab active" data-tab="config">
<tr> <i class="material-icons">settings</i> 설정
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifNm") %></th> </button>
<td><input type="text" name="name" /></td> <button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
</tr> <i class="material-icons">analytics</i> 현황
<tr> </button>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifDesc") %></th> </div>
<td><input type="text" name="desc" /></td>
</tr> <!-- 설정 탭 -->
<tr> <div class="tab-content active" id="tabConfigContent">
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th> <form id="ajaxForm">
<td><input type="text" name="thresholdPerSecond" /></td> <span class="section-label">기본 정보</span>
</tr> <table class="form-table" cellspacing="0">
<tr> <colgroup>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th> <col style="width:150px" />
<td><input type="text" name="threshold" /></td> <col />
</tr> </colgroup>
<tr> <tbody>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th> <tr>
<td> <th><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<div class="select-style"> <td><input type="text" name="name" style="width:300px;background-color: #ebebec;" /></td>
<select name="thresholdTimeUnit" /> </tr>
</div> <tr>
</td> <th><%= localeMessage.getString("infIfConMan.ifDesc") %></th>
</tr> <td><input type="text" name="desc" style="width:300px;background-color: #ebebec;" /></td>
<tr> </tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th> <tr>
<td> <th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<div class="select-style"> <td>
<select name="useYn" /> <select name="useYn" class="styled-select"></select>
</div> </td>
</td> </tr>
</tr> </tbody>
</table> </table>
</form>
</div><!-- end content_middle --> <span class="section-label">유량제어 설정</span>
</div><!-- end right_box --> <table class="form-table" cellspacing="0">
</body> <colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html> </html>
@@ -39,12 +39,22 @@
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />'; var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />'; var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />';
function dateFormat(cellvalue) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
$(document).ready(function () { $(document).ready(function () {
var key = "${param.id}"; var key = "${param.id}";
if (key) { if (key) {
detail(key); detail(key);
} }
$("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
$("#btn_app_approve").click(function () { $("#btn_app_approve").click(function () {
if (confirm("승인하시겠습니까?")) { if (confirm("승인하시겠습니까?")) {
$.ajax({ $.ajax({
@@ -113,6 +123,28 @@
} }
}); });
$("#btn_modify").click(function () {
if (confirm("수정하시겠습니까?")) {
$.ajax({
type: "POST",
url: url,
data: {cmd: "UPDATE", id: key, expectEndDate: $("#expectEndDate").val().replaceAll('-','')},
success: function (args) {
alert("수정되었습니다.");
if (window.dialogArguments && window.dialogArguments.self) {
window.dialogArguments.self.location.reload();
} else if (window.opener) {
window.opener.location.reload();
}
window.close();
},
error: function (e) {
alert(e.responseText);
}
});
}
});
$("#btn_close").click(function () { $("#btn_close").click(function () {
window.close(); window.close();
}); });
@@ -126,12 +158,25 @@
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (json) { success: function (json) {
var data = json; var data = json;
if (data.approvalStatus.code == 'END') {
$('#btn_modify').hide();
$('#expectEndDate').hide();
$('#spExpectEndDate').show();
$('#spExpectEndDate').text(dateFormat(data.expectEndDate));
} else {
$('#btn_modify').show();
$('#expectEndDate').show();
$('#spExpectEndDate').hide();
}
$("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용"); $("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용");
$("#approvalStatus").text(data.approvalStatus.description); $("#approvalStatus").text(data.approvalStatus.description);
$("#requesterName").text(data.requester.userName); $("#requesterName").text(data.requester.userName);
$("#approvalSubject").text(data.approvalSubject); $("#approvalSubject").text(data.approvalSubject);
$("#approvalDetail").html(data.approvalDetail); $("#approvalDetail").html(data.approvalDetail);
$("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true}); $("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true});
$("#expectEndDate").val(data.expectEndDate);
if (data.approvers.some(approver => if (data.approvers.some(approver =>
approver.approverStatus === 'CURRENT' && approver.approverStatus === 'CURRENT' &&
@@ -144,7 +189,7 @@
document.getElementById("btn_app_reject").style.display = "none"; document.getElementById("btn_app_reject").style.display = "none";
} }
if (data.approvalStatus.description === '배포실패') { if (data.approvalStatus.code === 'FAILED') {
document.getElementById("btn_app_redeploy").style.display = ""; document.getElementById("btn_app_redeploy").style.display = "";
} }
@@ -242,9 +287,14 @@
</tr> </tr>
<tr> <tr>
<th>상세 내용</th> <th>상세 내용</th>
<td colspan="3"> <td>
<div id="approvalDetail" style="white-space: pre-wrap;"></div> <div id="approvalDetail" style="white-space: pre-wrap;"></div>
</td> </td>
<th>예상완료일</th>
<td>
<input type="text" id="expectEndDate" style="width: 120px; text-align:center"/>
<span id="spExpectEndDate"></span>
</td>
</tr> </tr>
</table> </table>
@@ -282,6 +332,9 @@
</table> </table>
</div> </div>
<div class="popup_button"> <div class="popup_button">
<button type="button" class="cssbtn" id="btn_modify" level="W"><i
class="material-icons">save</i>수정
</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_app_approve" level="W"><i <button type="button" class="cssbtn" style="display: none;" id="btn_app_approve" level="W"><i
class="material-icons">check</i>승인 class="material-icons">check</i>승인
</button> </button>
@@ -23,6 +23,14 @@
'APP': 'API 사용' 'APP': 'API 사용'
}; };
function dateFormat(cellvalue, options, rowObject) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
function deleteSelectedApprovals() { function deleteSelectedApprovals() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow'); var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
@@ -74,7 +82,7 @@
searchApprovalStatus: $('select[name=searchApprovalStatus]').val(), searchApprovalStatus: $('select[name=searchApprovalStatus]').val(),
searchApprovalSubject: $('input[name=searchApprovalSubject]').val() searchApprovalSubject: $('input[name=searchApprovalSubject]').val()
}, },
colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일'], colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일', '예상완료일'],
colModel: [ colModel: [
{ name: 'rowNum', align: 'center', width: '30', sortable: false}, { name: 'rowNum', align: 'center', width: '30', sortable: false},
{ name: 'id', align: 'center', key: true, hidden: true }, { name: 'id', align: 'center', key: true, hidden: true },
@@ -83,7 +91,8 @@
{ name: 'approvalType', align: 'center', width: 80, hidden: true}, { name: 'approvalType', align: 'center', width: 80, hidden: true},
{ name: 'approvalStatus.description', align: 'center', width: 80 }, { name: 'approvalStatus.description', align: 'center', width: 80 },
{ name: 'requester.userName', align: 'center', width: 100 }, { name: 'requester.userName', align: 'center', width: 100 },
{ name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } } { name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } },
{ name: 'expectEndDate', align: 'center', width: 100, formatter: dateFormat},
], ],
jsonReader: { jsonReader: {
repeatitems: false repeatitems: false
@@ -189,8 +198,8 @@
<div class="select-style"> <div class="select-style">
<select name="searchApprovalStatus"> <select name="searchApprovalStatus">
<option value="">전체</option> <option value="">전체</option>
<option value="CREATED">생성됨</option>
<option value="REQUESTED">요청됨</option> <option value="REQUESTED">요청됨</option>
<option value="PROCESSING">진행중</option>
<option value="DENIED">반려됨</option> <option value="DENIED">반려됨</option>
<option value="END">승인됨</option> <option value="END">승인됨</option>
</select> </select>
@@ -20,15 +20,22 @@
var select_orgId = ""; var select_orgId = "";
function formatInquiryStatus(cellvalue, options, rowObject) { function formatInquiryStatus(cellvalue, options, rowObject) {
return cellvalue === 'PENDING' if (cellvalue == 'PENDING') {
? '<span style="color: red;">문의중</span>' return '<span style="color: red;">문의중</span>';
: '<span>답변완료</span>'; } else if (cellvalue == 'RESPONDED') {
return '<span >답변완료</span>'
} else if (cellvalue == 'CLOSED') {
return '<span >답변종료</span>'
} else {
return cellvalue;
}
} }
function formatFile(cellvalue, options, rowObject) { function formatFile(cellvalue, options, rowObject) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png'; var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : ''; var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
return cellvalue + icon; var commentCount = rowObject.commentCount > 0 ? ' <span style="color: #337ab7;">[' + rowObject.commentCount + ']</span>' : '';
return cellvalue + icon + commentCount;
} }
function deleteSelectedRows() { function deleteSelectedRows() {
@@ -37,6 +37,13 @@
border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */ border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */
} }
.comment-section {
margin-bottom: 30px;
border: 1px solid #e0e0e0; /* 동일한 박스 스타일 */
padding: 20px;
border-radius: 5px;
}
.info-row { .info-row {
margin-bottom: 10px; margin-bottom: 10px;
display: flex; display: flex;
@@ -58,6 +65,15 @@
min-height: 200px; min-height: 200px;
} }
#commentDetail {
min-height: 100px;
border: 1px solid #00000032;
border-radius: 4px;
padding: 10px;
flex: 1;
font-family: '맑은고딕'
}
.required-mark { .required-mark {
color: red; color: red;
margin-left: 2px; margin-left: 2px;
@@ -68,6 +84,7 @@
var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />'; var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />';
var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />'; var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />';
var file_download = '<c:url value="/file/download.do" />'; var file_download = '<c:url value="/file/download.do" />';
var inquiryStatus = 'PENDING';
function decodeHTMLEntities(text) { function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea'); var textArea = document.createElement('textarea');
@@ -82,10 +99,21 @@
dataType: "json", dataType: "json",
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (data) { success: function (data) {
$("#id").val(key); $("#id").val(key);
$("#inquirySubject").text(data.inquirySubject); $("#inquirySubject").text(data.inquirySubject);
$("#inquiryDetail").html(data.inquiryDetail); $("#inquiryDetail").html(data.inquiryDetail);
$("#inquiryStatus").text(data.inquiryStatus === 'PENDING' ? '문의중' : '답변완료');
inquiryStatus = data.inquiryStatus;
if (data.inquiryStatus == 'PENDING') {
$("#inquiryStatus").text('문의중');
} else if (data.inquiryStatus == 'RESPONDED') {
$("#inquiryStatus").text('답변완료');
} else if (data.inquiryStatus == 'CLOSED') {
$("#inquiryStatus").text('답변종료');
}
$("#inquirer").text(data.inquirer.userName); $("#inquirer").text(data.inquirer.userName);
$("#createdDate").text(timeStampFormat(data.createdDate)); $("#createdDate").text(timeStampFormat(data.createdDate));
$("#responder").text(data.responderName || ''); $("#responder").text(data.responderName || '');
@@ -94,6 +122,8 @@
var decodedContent = decodeHTMLEntities(data.responseDetail); var decodedContent = decodeHTMLEntities(data.responseDetail);
$("#responseDetail").summernote('code', decodedContent || ''); $("#responseDetail").summernote('code', decodedContent || '');
// 댓글 표시
listComment(key);
// 첨부 파일 표시 // 첨부 파일 표시
displayAttachFiles(data.fileInfo); displayAttachFiles(data.fileInfo);
@@ -105,6 +135,59 @@
}); });
} }
function listComment(inquiryId) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_COMMENT', id: inquiryId},
success: function (data) {
var $commentList = $('#commentList');
$commentList.empty();
if (!data || data.length === 0) return;
$.each(data, function (i, comment) {
var date = comment.createdDate ? timeStampFormat(comment.createdDate) : '';
var row = $('<div>').addClass('info-row');
$('<div>').addClass('info-label').css('color', '#555').text(comment.writerName || '').appendTo(row);
var content = $('<div>').addClass('info-content');
$('<div>').css('white-space', 'pre-wrap').text(comment.commentDetail).appendTo(content);
var meta = $('<div>').css({'font-size': '0.9em', 'color': '#999', 'margin-top': '2px'});
$('<span>').text(date).appendTo(meta);
$('<button>').attr('type', 'button').addClass('cssbtn smallBtn2').css({'margin-left': '8px', 'font-size': '0.85em', 'padding': '1px 6px'})
.text('delete').on('click', (function(id, inquiryId) {
return function() { deleteComment(id, inquiryId); };
})(comment.id, comment.inquiryId))
.appendTo(meta);
meta.appendTo(content);
content.appendTo(row);
row.appendTo($commentList);
});
},
error: function (e) {
alert(e.responseText);
}
});
}
function deleteComment(commentId, inquiryId) {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 삭제할 수 없습니다');
return;
}
if (!confirm("댓글을 삭제하시겠습니까?")) return;
$.ajax({
type: "POST",
url: url,
data: {cmd: 'DELETE_COMMENT', id: commentId},
success: function () {
listComment(inquiryId);
},
error: function (e) {
alert(e.responseText);
}
});
}
$(document).ready(function () { $(document).ready(function () {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key = "${param.id}"; var key = "${param.id}";
@@ -115,11 +198,16 @@
height: 200 height: 200
}); });
if (key) { if (key) {
detail(key); detail(key);
} }
$("#btn_modify").click(function () { $("#btn_modify").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 수정할 수 없습니다');
return;
}
if (!checkRequired("ajaxForm")) return; if (!checkRequired("ajaxForm")) return;
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return; if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
@@ -140,6 +228,32 @@
}); });
}); });
$("#btn_comment").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 댓글을 등록할 수 없습니다');
return;
}
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
$.ajax({
type: "POST",
url: url,
data: {
cmd: "INSERT_COMMENT",
inquiryId: $('#id').val(),
commentDetail: $('#commentDetail').val(),
},
success: function (json) {
alert("<%= localeMessage.getString("common.saveMsg") %>");
$('#commentDetail').val('');
listComment(key);
},
error: function (e) {
alert(e.responseText);
}
});
});
$("#btn_previous").click(function () { $("#btn_previous").click(function () {
goNav(returnUrl); goNav(returnUrl);
}); });
@@ -206,6 +320,10 @@
<!-- 답변 작성 부분 --> <!-- 답변 작성 부분 -->
<div class="response-section"> <div class="response-section">
<div class="info-row">
<div class="info-label">상태</div>
<div id="inquiryStatus" class="info-content"></div>
</div>
<div class="info-row"> <div class="info-row">
<div class="info-label">답변자</div> <div class="info-label">답변자</div>
<div id="responder" class="info-content"></div> <div id="responder" class="info-content"></div>
@@ -221,6 +339,19 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 댓글 작성 부분 -->
<div class="comment-section">
<div id="commentList"></div>
<div class="info-row">
<div class="info-label">${sessionScope.login.userName}</div>
<div style="flex:1; display: flex; gap: 8px; align-items: stretch;">
<textarea id="commentDetail" name="commentDetail" data-required data-warning="댓글 내용을 입력하여 주십시오."></textarea>
<button type="button" class="cssbtn" id="btn_comment" level="W" style="width:100px; height: 100px; text-align:center; ">댓글등록</button>
</div>
</div>
</div>
<input type="hidden" name="id" id="id"> <input type="hidden" name="id" id="id">
</form> </form>
</div> </div>
@@ -255,6 +255,13 @@ public interface MonitoringContext {
// 이중 로그인 허용여부 // 이중 로그인 허용여부
public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED"; public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED";
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 10)
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
// 웹훅 재전송 설정
public static final String API_WEBHOOK_RETRY_COUNT = "api.webhook.retry_count";
public static final String API_WEBHOOK_RETRY_TIME = "api.webhook.retry_time";
public abstract String getStringProperty(String propertyName); public abstract String getStringProperty(String propertyName);
@@ -617,6 +617,8 @@ public class MainController implements InterceptorSkipController {
menuId = menuId == null ? "" : menuId; menuId = menuId == null ? "" : menuId;
model.addAttribute("mainPage", mainPage); model.addAttribute("mainPage", mainPage);
model.addAttribute("menuId", menuId); model.addAttribute("menuId", menuId);
model.addAttribute("autoLogoutTimeout",
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 10));
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("CURRENT SERVICE TYPE : " + service); logger.debug("CURRENT SERVICE TYPE : " + service);
@@ -107,6 +107,17 @@ public class UserInfoService extends
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
public List<UserInfo> findAllByRoleId(String roleId) {
QUserInfo qUserInfo = QUserInfo.userInfo;
QUserRole qUserRole = QUserRole.userRole;
return getJPAQueryFactory()
.selectFrom(qUserInfo)
.join(qUserRole).on(qUserInfo.userid.eq(qUserRole.id.userId))
.where(qUserRole.id.roleId.eq(roleId))
.fetch();
}
public Page<UserInfo> findByUsername(Pageable pageable, String username) { public Page<UserInfo> findByUsername(Pageable pageable, String username) {
QUserInfo qUserInfo = QUserInfo.userInfo; QUserInfo qUserInfo = QUserInfo.userInfo;
BooleanExpression predicate = qUserInfo.isNotNull(); BooleanExpression predicate = qUserInfo.isNotNull();
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@EMSDataSource
public interface PortalInquiryCommentRepository extends BaseRepository<InquiryComment, String> {
long countByInquiry_Id(String inquiryId);
long countByInquiry_IdAndDelYn(String inquiryId, String delYn);
List<InquiryComment> findByInquiry_IdAndDelYnOrderByCreatedDateAsc(String inquiryId, String delYn);
Optional<InquiryComment> findTopByInquiry_IdAndDelYnOrderByCreatedDateDesc(
String inquiryId, String delYn);
Optional<InquiryComment> findTopByInquiry_IdAndAdminYnAndDelYnOrderByCreatedDateDesc(
String inquiryId, String adminYn, String delYn);
boolean existsByInquiry_IdAndAdminYnAndDelYnAndCreatedDateAfter(
String inquiryId, String adminYn, String delYn, LocalDateTime after);
}
@@ -0,0 +1,31 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalInquiryCommentService extends AbstractEMSDataSerivce<InquiryComment, String, PortalInquiryCommentRepository> {
// AbstractEMSDataSerivce의 repository 필드가 BaseRepository<T,I> 고정되어
// 커스텀 메서드를 호출할 없으므로 별도 필드로 직접 주입
@Autowired
private PortalInquiryCommentRepository inquiryCommentRepository;
public long countByInquiryId(String inquiryId) {
return inquiryCommentRepository.countByInquiry_Id(inquiryId);
}
public long countActiveByInquiryId(String inquiryId) {
return inquiryCommentRepository.countByInquiry_IdAndDelYn(inquiryId, "N");
}
public List<InquiryComment> findByInquiryId(String inquiryId) {
return inquiryCommentRepository.findByInquiry_IdAndDelYnOrderByCreatedDateAsc(inquiryId, "N");
}
}
@@ -4,7 +4,10 @@ import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import java.util.List;
@EMSDataSource @EMSDataSource
public interface PortalInquiryRepository extends BaseRepository<Inquiry, String> { public interface PortalInquiryRepository extends BaseRepository<Inquiry, String> {
List<Inquiry> findByInquiryStatus(String inquiryStatus);
} }
@@ -1,20 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry; package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
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.apim.portal.qna.entity.Inquiry; import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.apim.portal.qna.entity.QInquiry; import com.eactive.apim.portal.qna.entity.QInquiry;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce; import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service @Service
@Transactional(transactionManager = "transactionManagerForEMS") @Transactional(transactionManager = "transactionManagerForEMS")
public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String, PortalInquiryRepository> { public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String, PortalInquiryRepository> {
// AbstractEMSDataSerivce의 repository 필드가 BaseRepository<T,I> 고정되어
// 커스텀 메서드를 호출할 없으므로 별도 필드로 직접 주입
@Autowired
private PortalInquiryRepository portalInquiryRepository;
public Page<Inquiry> findAll(Pageable pageable, PortalInquirySearch portalInquirySearch) { public Page<Inquiry> findAll(Pageable pageable, PortalInquirySearch portalInquirySearch) {
QInquiry qInquiry = QInquiry.inquiry; QInquiry qInquiry = QInquiry.inquiry;
@@ -58,4 +66,8 @@ public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String
repository.deleteAll(inquiries); repository.deleteAll(inquiries);
} }
} }
public List<Inquiry> findByInquiryStatus(String inquiryStatus) {
return portalInquiryRepository.findByInquiryStatus(inquiryStatus);
}
} }
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.apistatus; package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -1,7 +1,8 @@
package com.eactive.eai.rms.data.ext.djb.apistatus; package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
public interface ApiStatusEvent { public interface ApiStatusEvent {
String getEaisvcname(); String getEaisvcname();
String getEaisvcdesc(); String getEaisvcdesc();
String getPrevStatusCode();
String getEvent(); String getEvent();
} }
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.data.entity.onl.djb.inflow;
public interface InflowTokenInsufficient { public interface InflowTokenInsufficient {
String getEaisvcname(); String getEaisvcname();
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable; import java.io.Serializable;
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable; import java.io.Serializable;
import javax.persistence.Column; import javax.persistence.Column;
@@ -0,0 +1,33 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "PTL_WEBHOOK_REQ")
@Getter
@NoArgsConstructor
public class WebhookReq {
@Id
@Column(name = "ID", length = 36, nullable = false)
private String id;
@Column(name = "ORG_ID", length = 100)
@Comment("법인 ID")
private String orgId;
@Column(name = "TARGET_URL", length = 500, nullable = false)
@Comment("웹훅 수신 URL")
private String targetUrl;
@Column(name = "SECRET", length = 200)
@Comment("HMAC 서명용 Secret Key")
private String secret;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_API")
@Getter
@NoArgsConstructor
public class WebhookReqApi implements Serializable {
@EmbeddedId
private WebhookReqApiId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqApiId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "API_ID", nullable = false, length = 100)
@Comment("API ID")
private String apiId;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_EVENT")
@Getter
@NoArgsConstructor
public class WebhookReqEvent implements Serializable {
@EmbeddedId
private WebhookReqEventId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqEventId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "EVENT_TYPE", nullable = false, length = 50)
@Comment("이벤트 유형 (CONTROL_START, ERROR_START 등)")
private String eventType;
}
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.webhook; package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -42,6 +42,9 @@ public class WebhookSendLog {
@Column(name = "STATUS_CODE") @Column(name = "STATUS_CODE")
private Integer statusCode; private Integer statusCode;
@Column(name = "ORG_ID")
private String orgId;
@Lob @Lob
@Column(name = "RESPONSE_BODY") @Column(name = "RESPONSE_BODY")
private String responseBody; private String responseBody;
@@ -1,69 +0,0 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
public void updateApiStatus(HashMap<String, String> param) {
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
Integer.parseInt(param.get("errorRate")),
Integer.parseInt(param.get("errorRangeMinute")),
Integer.parseInt(param.get("delayRangeMinute")),
Integer.parseInt(param.get("delayAvgRespTime"))
);
for (ApiStatusEvent event : list) {
String newStatusCode = resolveStatusCode(event.getEvent());
if (newStatusCode == null) continue;
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
eventPublisher.publishEvent(ApiStatusChangedEvent.from(event)); // 트랜잭션 커밋 리스너 실행
log.debug("이벤트 전파");
}
}
private String resolveStatusCode(String event) {
switch (event) {
case "CONTROL_START": return "C"; //점검
case "CONTROL_END": return "N"; //정상
case "ERROR_START": return "E"; //장애
case "ERROR_END": return "N"; //정상
case "DELAY_START": return "D"; //지연
case "DELAY_END": return "N"; //정상
default:
log.warn("알 수 없는 이벤트: {}", event);
return null;
}
}
}
@@ -0,0 +1,41 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class ApiStatusDetectionService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class);
/**
* 이벤트 감지
* @param event "CONTROL_START" : 점검시작
"CONTROL_END" : 점검종료
"ERROR_START" : 장애시작
"ERROR_END" : 장애종료
"DELAY_START" : 지연시작
"DELAY_END" : 지연종료
* @param apiIds API IDs
*/
public void detect(String event, List<String> apiIds) {
log.debug("이벤트 감지: {} {}", event, apiIds);
}
// API FSM 아직 장애로 남아있는 API 목록
public void remainDownApiIds() {
}
// 정상 동작 - detect()에서 일괄 처리 (복구 = 점검종료, 장애종료, 지연종료)
// public void recovered(String event, String[] apiIds) {
//
// }
}
@@ -1,10 +1,12 @@
package com.eactive.eai.rms.data.ext.djb.apistatus; package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> { public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
@@ -22,9 +24,11 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
@Query(nativeQuery = true, value = @Query(nativeQuery = true, value =
" SELECT EAISVCNAME" " SELECT EAISVCNAME"
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" + " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
+ " , PREV_STATUS_CODE "
+ " , EVENT" + " , EVENT"
+ " FROM (" + " FROM ("
+ " SELECT EAISVCNAME" + " SELECT EAISVCNAME"
+ " , STATUS_CODE AS PREV_STATUS_CODE "
+ " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CONTROL_START'" + " , 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 = 'C' AND CTRL_YN = 'N' THEN 'CONTROL_END'"
+ " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'" + " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'"
@@ -34,14 +38,15 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " ELSE 'STAY' END AS EVENT" + " ELSE 'STAY' END AS EVENT"
+ " FROM (" + " FROM ("
+ " SELECT A.EAISVCNAME" + " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, '1') AS STATUS_CODE" + " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
+ " , NVL((SELECT 'Y' FROM TSEAITI01" //TODO: 게시판 임시로 생성하여 사용. 유차장이 완료하면 변경할것
+ " WHERE (TO_CHAR(SYSDATE,'HH24MI') BETWEEN SUBSTR(EAICTRLDSTICCTNT,16,4) AND SUBSTR(EAICTRLDSTICCTNT,21,4)" + " , NVL(( SELECT 'Y' FROM PTL_NOTICE_API X "
+ " OR SUBSTR(EAICTRLDSTICCTNT,16,9) = '0000|0000')" + " WHERE EXISTS (SELECT 1 FROM EMSAPP.PTL_NOTICE Y WHERE X.NOTICE_ID = Y.ID)"
+ " AND A.EAISVCNAME = EAICTRLNAME),'N') AS CTRL_YN" //+ " AND TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI') BETWEEN START_TIME AND END_TIME) "
+ " AND A.EAISVCNAME = X.API_NAME),'N') AS CTRL_YN "
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'" + " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'" + " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END" + " ELSE 'N' END"
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL" + " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
+ " , SUM(SUCCESS_CNT) AS SUCCESS" + " , SUM(SUCCESS_CNT) AS SUCCESS"
+ " FROM API_STATS_MINUTE" + " FROM API_STATS_MINUTE"
@@ -0,0 +1,133 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private UmsManager ums;
@Autowired
private ApiStatusDetectionService apiStatusDectionService;
public void updateApiStatus(HashMap<String, String> param) {
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
Integer.parseInt(param.get("errorRate")),
Integer.parseInt(param.get("errorRangeMinute")),
Integer.parseInt(param.get("delayRangeMinute")),
Integer.parseInt(param.get("delayAvgRespTime"))
);
HashMap<String, List<String>> eventMap = new HashMap<>();
for (ApiStatusEvent event : list) {
String newStatusCode = resolveStatusCode(event.getEvent());
if (newStatusCode == null) continue;
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
//이벤트별로 api분리 저장
List<String> apis = (List<String>)eventMap.get(event.getEvent());
if (apis == null) {
apis = new ArrayList<String>();
apis.add(event.getEaisvcname());
eventMap.put(event.getEvent(), apis);
} else {
apis.add(event.getEaisvcname());
}
}
//이벤트별로 ums, webhook을 발송한다
for (Map.Entry<String, List<String>> entry : eventMap.entrySet()) {
String event = entry.getKey();
List<String> apiIds = entry.getValue();
String message = getSwingChatMessage(event, apiIds);
ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, message);
//제휴사 웹훅 발송
ums.sendWebhook(event, apiIds);
//개발자포탈 API상태모니터링 화면을 위한 처리
apiStatusDectionService.detect(event, apiIds);
}
}
private String resolveStatusCode(String event) {
switch (event) {
case "CONTROL_START": return "C"; //점검
case "CONTROL_END": return "N"; //정상
case "ERROR_START": return "E"; //장애
case "ERROR_END": return "N"; //정상
case "DELAY_START": return "D"; //지연
case "DELAY_END": return "N"; //정상
default:
log.warn("알 수 없는 이벤트: {}", event);
return null;
}
}
private String getSwingChatMessage(String event, List<String> apiIds) {
String message = "";
switch (event) {
case "CONTROL_START":
message = "API 서비스 점검이 시작되었습니다"; //점검
break;
case "CONTROL_END":
message = "API 서비스 점검이 완료되었습니다"; //정상
break;
case "ERROR_START":
message = "API 서비스 장애가 발생하였습니다"; //장애
break;
case "ERROR_END":
message = "API 서비스 장애가 복구되었습니다"; //정상
break;
case "DELAY_START":
message = "API 서비스 지연이 발생하였습니다"; //지연
break;
case "DELAY_END":
message = "API 서비스 지연이 복구되었습니다"; //정상
break;
default:
message = "";
}
return message + "\n- " + String.join(",", apiIds);
}
}
@@ -1,25 +0,0 @@
package com.eactive.eai.rms.ext.djb.async;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@@ -1,40 +0,0 @@
package com.eactive.eai.rms.ext.djb.async;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent;
import com.eactive.eai.rms.ext.djb.swing.service.SwingSendManager;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookSendManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@RequiredArgsConstructor
public class AsyncEventListener {
private final WebhookSendManager webhookSendManager;
private final SwingSendManager swingSendManager;
@Async("asyncExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onApiStatusChanged(ApiStatusChangedEvent event) {
log.debug("API 상태 변경 이벤트 수신: {} {}", event.getEaisvcname(), event.getEvent());
webhookSendManager.send(event.getEaisvcname(), event.getEvent());
swingSendManager.send(null, 0, 0);
}
@Async("asyncExecutor")
@EventListener // 트랜잭션 write 없이 read만 하므로 TransactionalEventListener 불필요
public void onInflowTokenFail(InflowTokenInsufficientEvent event) {
log.debug("유량제어 토큰획득 실패 이벤트 수신: {} {}건", event.getEaisvcname(), event.getCnt());
swingSendManager.send(event.getEaisvcname(), event.getCnt(), event.getRangeMinute());
}
}
@@ -1,18 +0,0 @@
package com.eactive.eai.rms.ext.djb.event;
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusEvent;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class ApiStatusChangedEvent {
private final String eaisvcname;
private final String eaisvcdesc;
private final String event;
public static ApiStatusChangedEvent from(ApiStatusEvent source) {
return new ApiStatusChangedEvent(source.getEaisvcname(), source.getEaisvcdesc(), source.getEvent());
}
}
@@ -1,19 +0,0 @@
package com.eactive.eai.rms.ext.djb.event;
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenInsufficient;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class InflowTokenInsufficientEvent {
private final String eaisvcname;
private final String eaisvcdesc;
private final long cnt;
private final long rangeMinute;
public static InflowTokenInsufficientEvent from(InflowTokenInsufficient summary, long rangeMinute) {
return new InflowTokenInsufficientEvent(summary.getEaisvcname(), summary.getEaisvcdesc(), summary.getCnt(), rangeMinute);
}
}
@@ -1,9 +1,12 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLog;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLogId;
public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> { public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> {
@@ -1,15 +1,17 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List; import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent; import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
@Service @Service
@@ -22,16 +24,16 @@ public class InflowTokenService {
private InflowTokenRepository inflowTokenRepository; private InflowTokenRepository inflowTokenRepository;
@Autowired @Autowired
private ApplicationEventPublisher eventPublisher; private UmsManager ums;
public void checkRecentFails(long rangeMinute) { public void checkRecentFails(long rangeMinute) {
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute); List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
for (InflowTokenInsufficient info : rows) { for (InflowTokenInsufficient info : rows) {
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt()); log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
String message = "유량제어 토큰 획득 실패하였습니다 \n" + info.getEaisvcname();
//내부직원 알림 발송 ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, message);
eventPublisher.publishEvent(InflowTokenInsufficientEvent.from(info, rangeMinute));
} }
} }
} }
@@ -17,7 +17,7 @@ import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil; import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusService; import com.eactive.eai.rms.ext.djb.apistatus.ApiStatusService;
import com.eactive.eai.rms.onl.common.util.DateUtil; import com.eactive.eai.rms.onl.common.util.DateUtil;
/** /**
@@ -88,11 +88,11 @@ public class ApiStatusMonitorJob implements Job {
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext"); monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
ApiStatusService apiStatusUpdateService = appContext.getBean(ApiStatusService.class); ApiStatusService apiStatusUpdateService = appContext.getBean(ApiStatusService.class);
DataSourceContextHolder.setDataSourceType( DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try { try {
apiStatusUpdateService.updateApiStatus(param); apiStatusUpdateService.updateApiStatus(param);
} catch (Exception e) { } catch (Exception e) {
log.error("ApiStatusUpdateJob execution failed", e); log.error("ApiStatusUpdateJob execution failed", e);
throw new JobExecutionException(e); throw new JobExecutionException(e);
@@ -18,7 +18,7 @@ import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil; import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenService; import com.eactive.eai.rms.ext.djb.inflow.InflowTokenService;
import com.eactive.eai.rms.onl.common.util.DateUtil; import com.eactive.eai.rms.onl.common.util.DateUtil;
/** /**
@@ -0,0 +1,73 @@
package com.eactive.eai.rms.ext.djb.job;
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.onl.apim.portalInquiry.PortalInquiryClosingService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 매일 00:05 실행 - RESPONDED 상태 문의글 자동 종료 배치
*
* <p>종료 조건 (기본 7일):</p>
* <ul>
* <li>답변완료 댓글이 7일간 없는 경우</li>
* <li>관리자 댓글 등록 7일간 포탈사용자 댓글이 없는 경우</li>
* </ul>
*
* <p>Cron: 0 5 0 * * ?</p>
*
* <p>Job 파라미터 (JobDataMap):</p>
* <ul>
* <li>{@value #KEY_INQUIRY_COMMENT_CLOSING_DAY}: 종료 기준 일수 (기본값: 7)</li>
* </ul>
*/
@DisallowConcurrentExecution
public class PortalInquiryClosingJob implements Job {
private static final Logger log = LoggerFactory.getLogger(PortalInquiryClosingJob.class);
/** Job 파라미터 키: 댓글종료 기간(일) */
public static final String KEY_INQUIRY_COMMENT_CLOSING_DAY = "inquiry.comment.closing_day";
private static final int DEFAULT_CLOSING_DAYS = 7;
@Autowired
private PortalInquiryClosingService inquiryCommentClosingService;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("*** START PortalInquiryCommentClosingService run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
int closingDays = DEFAULT_CLOSING_DAYS;
String closingDayParam = context.getMergedJobDataMap().getString(KEY_INQUIRY_COMMENT_CLOSING_DAY);
if (closingDayParam != null) {
try {
closingDays = Integer.parseInt(closingDayParam);
} catch (NumberFormatException e) {
log.warn("잘못된 {} 파라미터 값: '{}', 기본값 {}일 사용", KEY_INQUIRY_COMMENT_CLOSING_DAY, closingDayParam, DEFAULT_CLOSING_DAYS);
}
}
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
int closedCount = inquiryCommentClosingService.closeExpiredInquiries(closingDays);
log.info("PortalInquiryCommentClosingService 완료: {}건 CLOSED 처리", closedCount);
} catch (Exception e) {
log.error("PortalInquiryCommentClosingService execution failed", e);
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
log.info("*** END PortalInquiryCommentClosingService run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
}
}
@@ -1,17 +0,0 @@
package com.eactive.eai.rms.ext.djb.swing.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class SwingSendManager {
public void send(String eaisvcname, long cnt, long rangeMinute) {
log.info("[Swing] 발송 준비: {} 최근 {}분 {}건", eaisvcname, rangeMinute, cnt);
// TODO: 알림 발송 로직
}
}
@@ -0,0 +1,89 @@
package com.eactive.eai.rms.ext.djb.util;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class UmsManager {
private static final Logger log = LoggerFactory.getLogger(UmsManager.class);
private final MessageHandlerService messageHandlerService;
private final UserInfoService userInfoService;
private final WebhookService webhookService;
/**
* role 역할을 가진 내부직원에게 메신저(Swing chat) 발송
* @param role
* @param messageCode
* @param message
* @return
*/
public void sendMessenger(String roleId, MessageCode messageCode, String message) {
List<UserInfo> users = userInfoService.findAllByRoleId(roleId);
if (users.isEmpty()) {
log.info("sendMessenger: no users found for role '{}'", roleId);
return;
}
for (UserInfo user : users) {
MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid())
.username(user.getUsername())
.build();
HashMap<String, Object> params = new HashMap<>();
params.put("message", message);
messageHandlerService.publishEvent(messageCode, recipient, params);
}
}
/**
* 내부직원에게 메신저 발송
* @param user
* @param messageCode
* @param message
*/
public void sendMessenger(UserInfo user, MessageCode messageCode, String message) {
MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid())
.username(user.getUsername())
.build();
HashMap<String, Object> params = new HashMap<>();
params.put("message", message);
messageHandlerService.publishEvent(messageCode, recipient, params);
}
/**
* event를 구독중인 제휴사에게 웹훅 발송 (비동기)
* @param event
* @param apiIds
*/
@Async("webhookExecutor")
public void sendWebhook(String event, List<String> apiIds) {
List<WebhookSendRequest> list = webhookService.findSendList(event, apiIds);
for (WebhookSendRequest req : list) {
webhookService.send(req);
}
}
}
@@ -0,0 +1,26 @@
package com.eactive.eai.rms.ext.djb.webhook.config;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class WebhookAsyncConfig {
@Bean(name = "webhookExecutor")
public Executor webhookExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("webhook-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
@@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog; import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest; import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookReceiveService; import com.eactive.eai.rms.ext.djb.webhook.service.WebhookReceiveService;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService; import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
@@ -38,16 +38,12 @@ public class WebhookSendController {
public ResponseEntity<Map<String, Object>> sendWebhook( public ResponseEntity<Map<String, Object>> sendWebhook(
@RequestBody WebhookSendRequest request) { @RequestBody WebhookSendRequest request) {
WebhookSendLog result = webhookService.send( webhookService.send(request);
request.getTargetUrl(),
request.getEventType(),
request.getData()
);
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("logId", result.getId()); // response.put("logId", result.getId());
response.put("success", result.getSuccess()); // response.put("success", result.getSuccess());
response.put("statusCode", result.getStatusCode()); // response.put("statusCode", result.getStatusCode());
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@@ -6,8 +6,9 @@ import lombok.Setter;
@Getter @Getter
@Setter @Setter
public class WebhookSendRequest { public class WebhookSendRequest {
private String orgId;
private String targetUrl; private String targetUrl;
private String eventType; private String eventType;
private String secret;
private Object data; private Object data;
} }
@@ -0,0 +1,11 @@
package com.eactive.eai.rms.ext.djb.webhook.repository;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookReq;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
@EMSDataSource
public interface WebhookReqRepository extends JpaRepository<WebhookReq, String> {
}
@@ -10,7 +10,7 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog; import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
@Repository @Repository
@EMSDataSource @EMSDataSource
@@ -1,17 +0,0 @@
package com.eactive.eai.rms.ext.djb.webhook.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class WebhookSendManager {
public void send(String eaisvcname, String eventType) {
log.info("[Webhook] 발송 준비: {} {}", eaisvcname, eventType);
// TODO: PTL_WEBHOOK_REQ 조인 조회 WebhookService.send() 호출
}
}
@@ -2,12 +2,16 @@ package com.eactive.eai.rms.ext.djb.webhook.service;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -18,10 +22,17 @@ import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog; import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqApi;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqEvent;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookPayload; import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookPayload;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository; import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -34,19 +45,63 @@ public class WebhookService {
private final WebhookSendLogRepository sendLogRepository; private final WebhookSendLogRepository sendLogRepository;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final MonitoringContext monitoringContext;
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf"; private EntityManager entityManager;
private static final String HMAC_ALGORITHM = "HmacSHA256"; private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final int MAX_RETRY = 3; private static final int DEFAULT_RETRY_COUNT = 3;
private static final int DEFAULT_RETRY_TIME = 10000;
@SuppressWarnings("unchecked")
public List<WebhookSendRequest> findSendList(String event, List<String> apiIds) {
QWebhookReq req = QWebhookReq.webhookReq;
QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent;
QWebhookReqApi api = QWebhookReqApi.webhookReqApi;
List<Tuple> tuples = new JPAQueryFactory(entityManager)
.select(req.orgId, req.targetUrl, req.secret, api.id.apiId)
.from(req)
.join(evt).on(req.id.eq(evt.id.webhookReqId))
.join(api).on(req.id.eq(api.id.webhookReqId))
.where(evt.id.eventType.eq(event)
.and(api.id.apiId.in(apiIds)))
.fetch();
// orgId + targetUrl + secret 기준으로 그룹핑, apiId를 data(List) 집계
Map<String, WebhookSendRequest> resultMap = new LinkedHashMap<>();
for (Tuple t : tuples) {
String key = t.get(req.orgId) + "|" + t.get(req.targetUrl);
resultMap.computeIfAbsent(key, k -> {
WebhookSendRequest wr = new WebhookSendRequest();
wr.setOrgId(t.get(req.orgId));
wr.setTargetUrl(t.get(req.targetUrl));
wr.setSecret(t.get(req.secret));
wr.setEventType(event);
wr.setData(new ArrayList<String>());
return wr;
});
((List<String>) resultMap.get(key).getData()).add(t.get(api.id.apiId));
}
return new ArrayList<>(resultMap.values());
}
/** /**
* 웹훅 발송 메인 메서드 * 웹훅 발송 메인 메서드
*/ */
@Transactional @Transactional
public WebhookSendLog send(String targetUrl, String eventType, Object data) { public void send(WebhookSendRequest req) {
String targetUrl = req.getTargetUrl();
String secret = req.getSecret();
String eventType = req.getEventType();
Object data = req.getData();
WebhookSendLog sendLog = new WebhookSendLog(); WebhookSendLog sendLog = new WebhookSendLog();
sendLog.setOrgId(req.getOrgId());
sendLog.setTargetUrl(targetUrl); sendLog.setTargetUrl(targetUrl);
sendLog.setEventType(eventType); sendLog.setEventType(eventType);
@@ -57,7 +112,7 @@ public class WebhookService {
sendLog.setPayload(payloadJson); sendLog.setPayload(payloadJson);
// 2. Secret Key로 HMAC-SHA256 서명 생성 // 2. Secret Key로 HMAC-SHA256 서명 생성
String signature = generateSignature(payloadJson); String signature = generateSignature(secret, payloadJson);
sendLog.setSignature(signature); sendLog.setSignature(signature);
// 3. HTTP 요청 헤더 구성 // 3. HTTP 요청 헤더 구성
@@ -67,18 +122,20 @@ public class WebhookService {
headers.set("X-Webhook-Event", eventType); headers.set("X-Webhook-Event", eventType);
headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp())); headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp()));
// 4. 발송 // 4. 발송 (재시도 포함)
int retryCount = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_COUNT, DEFAULT_RETRY_COUNT);
int retryTime = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME);
sendLog.setSentAt(LocalDateTime.now()); sendLog.setSentAt(LocalDateTime.now());
HttpEntity<String> request = new HttpEntity<>(payloadJson, headers); HttpEntity<String> httpRequest = new HttpEntity<>(payloadJson, headers);
ResponseEntity<String> response = restTemplate.postForEntity(targetUrl, request, String.class); ResponseEntity<String> response = sendWithRetry(targetUrl, httpRequest, sendLog, retryCount, retryTime);
// 5. 성공 로그 // 5. 성공 로그
sendLog.setStatusCode(response.getStatusCode().value()); sendLog.setStatusCode(response.getStatusCode().value());
sendLog.setResponseBody(response.getBody()); sendLog.setResponseBody(response.getBody());
sendLog.setSuccess(response.getStatusCode().is2xxSuccessful()); sendLog.setSuccess(response.getStatusCode().is2xxSuccessful());
log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}", log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}, retryCount: {}",
eventType, targetUrl, response.getStatusCode()); eventType, targetUrl, response.getStatusCode(), sendLog.getRetryCount());
} catch (HttpClientErrorException | HttpServerErrorException e) { } catch (HttpClientErrorException | HttpServerErrorException e) {
sendLog.setStatusCode(e.getStatusCode().value()); sendLog.setStatusCode(e.getStatusCode().value());
@@ -95,40 +152,46 @@ public class WebhookService {
eventType, targetUrl, e.getMessage()); eventType, targetUrl, e.getMessage());
} }
return sendLogRepository.save(sendLog); sendLogRepository.save(sendLog);
} }
/** /**
* 실패 재시도 * 재시도 포함 HTTP 발송
* - 4xx: 클라이언트 오류이므로 즉시 throw (재시도 불필요)
* - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 exponential backoff 재시도 (1s 2s 4s)
*/ */
@Transactional private ResponseEntity<String> sendWithRetry(String targetUrl, HttpEntity<String> request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception {
public void retryFailedWebhooks() { Exception lastException = null;
List<WebhookSendLog> failedLogs = for (int attempt = 0; attempt <= retryCount; attempt++) {
sendLogRepository.findFailedLogs(MAX_RETRY);
for (WebhookSendLog failedLog : failedLogs) {
try { try {
log.info("[Webhook] 재시도 - id: {}, retry: {}", failedLog.getId(), failedLog.getRetryCount()); if (attempt > 0) {
failedLog.setRetryCount(failedLog.getRetryCount() + 1); long delayMs = retryTime * (1L << (attempt - 1));
sendLogRepository.save(failedLog); log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, targetUrl, delayMs);
Thread.sleep(delayMs);
send(failedLog.getTargetUrl(), failedLog.getEventType(), sendLog.setRetryCount(attempt);
objectMapper.readValue(failedLog.getPayload(), Object.class)); }
return restTemplate.postForEntity(targetUrl, request, String.class);
} catch (HttpClientErrorException e) {
throw e; // 4xx는 재시도 불필요
} catch (Exception e) { } catch (Exception e) {
log.error("[Webhook] 재시도 실패 - id: {}", failedLog.getId(), e); lastException = e;
log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, targetUrl, e.getMessage());
} }
} }
throw lastException;
} }
/** /**
* HMAC-SHA256 서명 생성 * HMAC-SHA256 서명 생성
* Java 17 미만은 HexFormat 미지원이므로 직접 변환 * Java 17 미만은 HexFormat 미지원이므로 직접 변환
*/ */
private String generateSignature(String payload) throws Exception { private String generateSignature(String secret, String payload) throws Exception {
Mac mac = Mac.getInstance(HMAC_ALGORITHM); Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec( SecretKeySpec keySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM); secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(keySpec); mac.init(keySpec);
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash); return bytesToHex(hash);
@@ -124,4 +124,10 @@ public class PortalApprovalManController extends BaseAnnotationController {
} }
return ResponseEntity.ok(resultMap); return ResponseEntity.ok(resultMap);
} }
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=UPDATE")
public ResponseEntity<Void> update(String id, String expectEndDate) {
portalApprovalManService.update(id, expectEndDate);
return ResponseEntity.ok().build();
}
} }
@@ -87,6 +87,16 @@ public class PortalApprovalManService extends BaseService {
* 마스킹된 상세 정보 조회 * 마스킹된 상세 정보 조회
*/ */
public PortalApprovalUI selectDetail(String id) { public PortalApprovalUI selectDetail(String id) {
//2026.06.05 관리자가 승인요청을 조회시 심사중으로 상태 변경
//APP 유형이고 REQUESTED 상태인 경우 관리자 조회 PROCESSING으로 변경
approvalService.findById(id).ifPresent(approval -> {
if (approval.getApprovalType().equals(ApprovalType.APP)
&& approval.getApprovalStatus() instanceof RequestedState) {
approval.setApprovalStatus(new ProcessingState());
approvalService.save(approval);
}
});
PortalApprovalUI result = getDetailWithMaskOption(id, false); PortalApprovalUI result = getDetailWithMaskOption(id, false);
if (result == null) { if (result == null) {
throw new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id); throw new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id);
@@ -331,6 +341,13 @@ public class PortalApprovalManService extends BaseService {
sendEvent(id, ApprovalEvent.DENY, options); sendEvent(id, ApprovalEvent.DENY, options);
} }
public void update(String id, String expectEndDate) {
Approval approval = approvalService.findById(id)
.orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
approval.setExpectEndDate(expectEndDate);
approvalService.save(approval);
}
public void redeploy(String id) { public void redeploy(String id) {
Map options = new HashMap<>(); Map options = new HashMap<>();
Approval approval = approvalService.findById(id).orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id)); Approval approval = approvalService.findById(id).orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
@@ -38,6 +38,8 @@ public class PortalApprovalUI {
@DateTimeFormat(pattern = "yyyyMMddHHmmssSSS") @DateTimeFormat(pattern = "yyyyMMddHHmmssSSS")
private String approvalDate; private String approvalDate;
private String expectEndDate;
public String getApprovalTypeDescription() { public String getApprovalTypeDescription() {
return approvalType != null ? approvalType.getDescription() : ""; return approvalType != null ? approvalType.getDescription() : "";
} }
@@ -0,0 +1,78 @@
package com.eactive.eai.rms.onl.apim.portalInquiry;
import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryCommentRepository;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryCommentService;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* RESPONDED 상태 문의글 아래 조건에 해당하는 경우 CLOSED로 변경한다.
* - 조건 A: 활성 댓글이 없고 답변완료일(responseDate)로부터 N일 경과
* - 조건 B: 마지막 관리자 댓글(adminYn=Y) 이후 N일간 포탈사용자(adminYn=N) 댓글 없음
*/
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
@Slf4j
@RequiredArgsConstructor
public class PortalInquiryClosingService {
private static final String RESPONDED = "RESPONDED";
private static final String CLOSED = "CLOSED";
private final PortalInquiryService portalInquiryService;
private final PortalInquiryCommentService portalInquiryCommentService;
private final PortalInquiryCommentRepository inquiryCommentRepository;
public int closeExpiredInquiries(int closingDays) {
LocalDateTime threshold = LocalDateTime.now().minusDays(closingDays);
List<Inquiry> respondedInquiries = portalInquiryService.findByInquiryStatus(RESPONDED);
int closedCount = 0;
for (Inquiry inquiry : respondedInquiries) {
if (shouldClose(inquiry, threshold)) {
inquiry.setInquiryStatus(CLOSED);
portalInquiryService.save(inquiry);
closedCount++;
log.info("문의글 CLOSED 처리: id={}", inquiry.getId());
}
}
log.info("InquiryCommentClosingService: 총 {}건 CLOSED 처리 (기준={}일)", closedCount, closingDays);
return closedCount;
}
private boolean shouldClose(Inquiry inquiry, LocalDateTime threshold) {
long activeCommentCount = portalInquiryCommentService.countActiveByInquiryId(inquiry.getId());
// 댓글 없음 답변완료일 기준 N일 경과 여부만 확인
if (activeCommentCount == 0) {
LocalDateTime responseDate = inquiry.getResponseDate();
return responseDate != null && !responseDate.isAfter(threshold);
}
// 가장 최근 활성 댓글 확인
Optional<InquiryComment> lastComment = inquiryCommentRepository
.findTopByInquiry_IdAndDelYnOrderByCreatedDateDesc(inquiry.getId(), "N");
if (!lastComment.isPresent()) {
return false;
}
// 포탈사용자(adminYn != 'Y') 마지막 댓글 작성자 CLOSED 불가
if (!"Y".equals(lastComment.get().getAdminYn())) {
return false;
}
// 관리자가 마지막 댓글 작성자 N일 경과 여부 확인
LocalDateTime lastAdminDate = lastComment.get().getCreatedDate();
return lastAdminDate != null && !lastAdminDate.isAfter(threshold);
}
}
@@ -0,0 +1,40 @@
package com.eactive.eai.rms.onl.apim.portalInquiry;
import com.eactive.apim.portal.file.entity.FileInfo;
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class PortalInquiryCommentUI {
private String id;
private String inquiryId;
private String commentDetail;
private String delYn;
private String createdBy;
private String writerName;
private String lastModifiedBy;
@JsonFormat(pattern = LocalDateTimeFormatters.FORMAT_YYYYMMDDHHMMSS_14)
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime createdDate;
@JsonFormat(pattern = LocalDateTimeFormatters.FORMAT_YYYYMMDDHHMMSS_14)
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime lastModifiedDate;
}
@@ -0,0 +1,17 @@
package com.eactive.eai.rms.onl.apim.portalInquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import java.util.List;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface PortalInquiryCommentUIMapper {
@Mapping(target = "inquiryId", source = "inquiry.id")
PortalInquiryCommentUI toVo(InquiryComment entity);
List<PortalInquiryCommentUI> toVoList(List<InquiryComment> entities);
}
@@ -74,4 +74,22 @@ public class PortalInquiryManController extends BaseAnnotationController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PostMapping(value = "/onl/apim/portalinquiry/portalInquiryMan.json", params = "cmd=LIST_COMMENT")
public ResponseEntity<List<PortalInquiryCommentUI>> selectCommentList(String id) {
return ResponseEntity.ok(portalInquiryManService.selectCommentList(id));
}
@PostMapping(value = "/onl/apim/portalinquiry/portalInquiryMan.json", params = "cmd=INSERT_COMMENT")
public ResponseEntity<String> insertComment(HttpServletRequest request, PortalInquiryCommentUI portalInquiryCommentUI) {
String userid = SessionManager.getUserId(request);
portalInquiryManService.insertComment(portalInquiryCommentUI, userid);
return ResponseEntity.ok().build();
}
@PostMapping(value = "/onl/apim/portalinquiry/portalInquiryMan.json", params = "cmd=DELETE_COMMENT")
public ResponseEntity<Void> deleteComment(String id) {
portalInquiryManService.deleteComment(id);
return ResponseEntity.ok().build();
}
} }
@@ -4,10 +4,14 @@ import com.eactive.apim.portal.file.entity.FileInfo;
import com.eactive.apim.portal.file.service.FileService; import com.eactive.apim.portal.file.service.FileService;
import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.qna.entity.Inquiry; import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.apim.portal.user.entity.UserInfo; import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.common.base.BaseService; import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.exception.IntegrationException;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService; import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryCommentService;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquirySearch; import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquirySearch;
import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryService; import com.eactive.eai.rms.data.entity.onl.apim.portalinquiry.PortalInquiryService;
import com.eactive.eai.rms.onl.apim.masking.MaskingUtils; import com.eactive.eai.rms.onl.apim.masking.MaskingUtils;
@@ -22,27 +26,38 @@ import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
@Service @Service
@Transactional(transactionManager = "transactionManagerForEMS") @Transactional(transactionManager = "transactionManagerForEMS")
@Slf4j @Slf4j
public class PortalInquiryManService extends BaseService { public class PortalInquiryManService extends BaseService {
private final PortalInquiryService portalInquiryService; private final PortalInquiryService portalInquiryService;
private final PortalInquiryCommentService portalInquiryCommentService;
private final PortalInquiryUIMapper portalInquiryUIMapper; private final PortalInquiryUIMapper portalInquiryUIMapper;
private final PortalInquiryCommentUIMapper portalInquiryCommentUIMapper;
private final UserInfoService userInfoService; private final UserInfoService userInfoService;
private final PortalUserRepository portalUserRepository;
private final FileService fileService; private final FileService fileService;
public static final String PENDING = "PENDING"; public static final String PENDING = "PENDING";
public static final String RESPONDED = "RESPONDED"; public static final String RESPONDED = "RESPONDED";
public static final String CLOSED = "CLOSED";
public PortalInquiryManService(PortalInquiryService portalInquiryService, public PortalInquiryManService(PortalInquiryService portalInquiryService,
PortalInquiryCommentService portalInquiryCommentService,
PortalInquiryUIMapper portalInquiryUIMapper, PortalInquiryUIMapper portalInquiryUIMapper,
PortalInquiryCommentUIMapper portalInquiryCommentUIMapper,
UserInfoService userInfoService, UserInfoService userInfoService,
PortalUserRepository portalUserRepository,
FileService fileService){ FileService fileService){
this.portalInquiryService = portalInquiryService; this.portalInquiryService = portalInquiryService;
this.portalInquiryCommentService = portalInquiryCommentService;
this.portalInquiryUIMapper = portalInquiryUIMapper; this.portalInquiryUIMapper = portalInquiryUIMapper;
this.portalInquiryCommentUIMapper = portalInquiryCommentUIMapper;
this.userInfoService = userInfoService; this.userInfoService = userInfoService;
this.portalUserRepository = portalUserRepository;
this.fileService = fileService; this.fileService = fileService;
} }
@@ -55,6 +70,7 @@ public class PortalInquiryManService extends BaseService {
if (ui.getInquirer() != null) { if (ui.getInquirer() != null) {
maskPortalInquiryUI(ui); maskPortalInquiryUI(ui);
} }
ui.setCommentCount((int) portalInquiryCommentService.countActiveByInquiryId(inquiry.getId()));
return setInquiryAttachmentStatus(ui); return setInquiryAttachmentStatus(ui);
}); });
} }
@@ -141,6 +157,7 @@ public class PortalInquiryManService extends BaseService {
UserInfo userinfo = userInfoService.getById(userId); UserInfo userinfo = userInfoService.getById(userId);
Inquiry inquiry = portalInquiryService.getById(portalInquiryUI.getId()); Inquiry inquiry = portalInquiryService.getById(portalInquiryUI.getId());
validateNotClosed(inquiry);
// 답변 대기중 상태일 경우만 답변완료로 상태 변경 // 답변 대기중 상태일 경우만 답변완료로 상태 변경
if(inquiry.getInquiryStatus().equals(PENDING)) { if(inquiry.getInquiryStatus().equals(PENDING)) {
inquiry.setInquiryStatus(RESPONDED); inquiry.setInquiryStatus(RESPONDED);
@@ -162,4 +179,52 @@ public class PortalInquiryManService extends BaseService {
portalInquiryService.deleteByIds(idList); portalInquiryService.deleteByIds(idList);
} }
public List<PortalInquiryCommentUI> selectCommentList(String inquiryId) {
List<InquiryComment> comments = portalInquiryCommentService.findByInquiryId(inquiryId);
List<PortalInquiryCommentUI> result = portalInquiryCommentUIMapper.toVoList(comments);
result.forEach(ui -> ui.setWriterName(resolveWriterName(ui.getCreatedBy())));
return result;
}
private String resolveWriterName(String userId) {
if (StringUtils.isBlank(userId)) return "";
// 직원(내부 사용자) 여부 먼저 확인 이름 그대로 반환
Optional<UserInfo> staff = userInfoService.findById(userId);
if (staff.isPresent()) {
return staff.get().getUsername();
}
// 포털 회원이면 이름 마스킹 처리 (김수민 김수*)
Optional<PortalUser> portalUser = portalUserRepository.findById(userId);
if (portalUser.isPresent()) {
return MaskingUtils.maskName(portalUser.get().getUserName());
}
return userId;
}
public void insertComment(PortalInquiryCommentUI commentUI, String userId) {
Inquiry inquiry = portalInquiryService.getById(commentUI.getInquiryId());
validateNotClosed(inquiry);
InquiryComment comment = new InquiryComment();
comment.setInquiry(inquiry);
comment.setCommentDetail(commentUI.getCommentDetail());
comment.setDelYn("N");
comment.setAdminYn("Y");
portalInquiryCommentService.save(comment);
}
public void deleteComment(String commentId) {
InquiryComment comment = portalInquiryCommentService.getById(commentId);
validateNotClosed(comment.getInquiry());
comment.setDelYn("Y");
portalInquiryCommentService.save(comment);
}
private void validateNotClosed(Inquiry inquiry) {
if (CLOSED.equals(inquiry.getInquiryStatus())) {
throw new IntegrationException("답변종료되어 수정할 수 없습니다.");
}
}
} }
@@ -55,4 +55,6 @@ public class PortalInquiryUI {
private LocalDateTime lastModifiedDate; private LocalDateTime lastModifiedDate;
private String orgName; private String orgName;
private int commentCount;
} }
@@ -1,19 +1,5 @@
package com.eactive.eai.rms.onl.common.service.ums; package com.eactive.eai.rms.onl.common.service.ums;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
import com.eactive.ext.kjb.ums.KjbEmailSendModule;
import com.eactive.ext.kjb.ums.KjbUmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
@@ -21,6 +7,23 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
import com.eactive.ext.djb.ums.DjbMessengerService;
import com.eactive.ext.kjb.ums.KjbEmailSendModule;
import com.eactive.ext.kjb.ums.KjbUmsService;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@@ -68,14 +71,18 @@ public class UmsDispatchService {
log.info("UmsDispatchService initialized - KjbUmsService and KjbEmailSendModule ready via singleton"); log.info("UmsDispatchService initialized - KjbUmsService and KjbEmailSendModule ready via singleton");
} }
// private KjbUmsService getUmsService() { private DjbMessengerService getMessengerService() {
// return KjbUmsService.getInstance(); return DjbMessengerService.getInstance();
// } }
//
// private KjbEmailSendModule getEmailModule() { private KjbUmsService getUmsService() {
// return KjbEmailSendModule.getInstance(); return KjbUmsService.getInstance();
// } }
//
private KjbEmailSendModule getEmailModule() {
return KjbEmailSendModule.getInstance();
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Transactional(transactionManager = "transactionManagerForEMS") @Transactional(transactionManager = "transactionManagerForEMS")
public void processMessages() { public void processMessages() {
@@ -112,7 +119,7 @@ public class UmsDispatchService {
log.info("Starting to submit {} messages for processing", messagesToProcess.size()); log.info("Starting to submit {} messages for processing", messagesToProcess.size());
for (MessageRequest message : messagesToProcess) { for (MessageRequest message : messagesToProcess) {
//submitMessageTask(message); submitMessageTask(message);
} }
} else { } else {
log.info("No pending messages found"); log.info("No pending messages found");
@@ -123,62 +130,45 @@ public class UmsDispatchService {
} }
// private void submitMessageTask(MessageRequest message) { private void submitMessageTask(MessageRequest message) {
// int queueSize = executorService.getQueue().size(); int queueSize = executorService.getQueue().size();
// log.debug("Current queue size before submission: {}", queueSize); log.debug("Current queue size before submission: {}", queueSize);
//
// executorService.submit(() -> { executorService.submit(() -> {
// try { try {
// transactionTemplate.execute(status -> { transactionTemplate.execute(status -> {
// try { try {
// // 광주은행에서는 사용하지 않는 메소드 if (DjbMessengerService.isAllowChannel(message)) {
//// processMessage(message); getMessengerService().send(message);
// }
// if (KjbUmsService.isAllowChannel(message)) { if (KjbUmsService.isAllowChannel(message)) {
// getUmsService().send(message); getUmsService().send(message);
// } }
// if (KjbEmailSendModule.isAllowChannel(message)) {
// if (KjbEmailSendModule.isAllowChannel(message)) { getEmailModule().sendEmail(message);
// getEmailModule().sendEmail(message); }
// } updateMessageStatus(message, "SENT");
// return null;
// return null; } catch (Exception e) {
// } catch (Exception e) { // checked exception을 RuntimeException으로 감싸 전파 TransactionTemplate이 rollback re-throw
// log.error("Failed to process message: {} - Error: {}", throw new RuntimeException(e);
// message.getId(), e.getMessage(), e); }
// updateMessageStatus(message, "FAILED"); });
// status.setRollbackOnly(); } catch (Exception e) {
// return null; log.error("Failed to process message: {} - Error: {}", message.getId(), e.getMessage(), e);
// } try {
// }); transactionTemplate.execute(s -> {
// } catch (Exception e) { updateMessageStatus(message, "FAILED");
// log.error("Transaction execution failed for message: {} - Error: {}", return null;
// message.getId(), e.getMessage(), e); });
// } } catch (Exception ex) {
// }); log.error("Failed to update FAIL status for message: {}", message.getId(), ex);
// } }
}
});
}
/**
* kbank 방식으로 광주은행에서는 사용하지 않음
* @param message
* @throws IOException
*/
// @Deprecated
// public void processMessage(MessageRequest message) throws IOException {
// try {
// String standardMessage = messageFactory.createMessage(message);
// log.debug("Processing message ID: {} - Type: {}", message.getId(), message.getMessageType());
//
// umsSender.sendMessage(standardMessage);
// updateMessageStatus(message, "SENT");
// log.debug("Successfully processed and sent message ID: {}", message.getId());
// } catch (Exception e) {
// log.error("Message processing failed for ID: {} - Error: {}",
// message.getId(), e.getMessage(), e);
// updateMessageStatus(message, "FAILED");
// throw e;
// }
// }
public void updateMessageStatus(MessageRequest message, String status) { public void updateMessageStatus(MessageRequest message, String status) {
message.setRequestStatus(status); message.setRequestStatus(status);
@@ -85,4 +85,12 @@ public class InflowAdapterControlManController extends OnlBaseAnnotationControll
return new ModelAndView("jsonView", resultMap); return new ModelAndView("jsonView", resultMap);
} }
@RequestMapping(value = "/onl/admin/inflow/inflowAdapterControlMan.json", params = "cmd=LIST_BUCKET_STATUS")
public ModelAndView getBucketStatus(HttpServletRequest request, HttpServletResponse response, String adapterId) {
List<Map<String, Object>> statusList = service.getAdapterBucketStatus(adapterId);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("servers", statusList);
return new ModelAndView("jsonView", resultMap);
}
} }
@@ -83,4 +83,12 @@ public class InflowClientControlManController extends OnlBaseAnnotationControlle
return new ModelAndView("jsonView", resultMap); return new ModelAndView("jsonView", resultMap);
} }
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=LIST_BUCKET_STATUS")
public ModelAndView getBucketStatus(HttpServletRequest request, HttpServletResponse response, String clientId) {
List<Map<String, Object>> statusList = service.getClientBucketStatus(clientId);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("servers", statusList);
return new ModelAndView("jsonView", resultMap);
}
} }
@@ -1,12 +1,21 @@
package com.eactive.eai.rms.onl.manage.inflow.inflow; package com.eactive.eai.rms.onl.manage.inflow.inflow;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.eactive.eai.data.entity.onl.inflow.InflowControl; import com.eactive.eai.data.entity.onl.inflow.InflowControl;
import com.eactive.eai.data.entity.onl.inflow.InflowControlId; import com.eactive.eai.data.entity.onl.inflow.InflowControlId;
@@ -14,10 +23,13 @@ import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.combo.ComboService; import com.eactive.eai.rms.common.combo.ComboService;
import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlService; import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlService;
import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlServiceDto; import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlServiceDto;
import com.eactive.eai.rms.onl.common.service.EaiServerInfoService;
@Service @Service
public class InflowControlManService extends BaseService { public class InflowControlManService extends BaseService {
private static final Logger logger = LoggerFactory.getLogger(InflowControlManService.class);
private static final String ADAPTER_TYPE_INFLOW = "01"; private static final String ADAPTER_TYPE_INFLOW = "01";
private static final String INTERFACE_TYPE_INFLOW = "02"; private static final String INTERFACE_TYPE_INFLOW = "02";
private static final String CLIENT_TYPE_INFLOW = "03"; private static final String CLIENT_TYPE_INFLOW = "03";
@@ -32,6 +44,11 @@ public class InflowControlManService extends BaseService {
@Autowired @Autowired
private InflowControlManMapper mapper; private InflowControlManMapper mapper;
@Autowired
private EaiServerInfoService eaiServerInfoService;
private RestTemplate restTemplate = new RestTemplate();
// 어댑터 유량제어 // 어댑터 유량제어
public Page<InflowControlManUI> selectAdapterList(Pageable pageable, String searchName) { public Page<InflowControlManUI> selectAdapterList(Pageable pageable, String searchName) {
Page<InflowControlServiceDto> dtoList = service.findAllForAdapter(pageable, searchName); Page<InflowControlServiceDto> dtoList = service.findAllForAdapter(pageable, searchName);
@@ -124,6 +141,64 @@ public class InflowControlManService extends BaseService {
service.findById(id).ifPresent(service::delete); service.findById(id).ifPresent(service::delete);
} }
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getClientBucketStatus(String clientId) {
return getBucketStatus("client", clientId, "클라이언트가 로드되지 않음", "클라이언트 버킷 상태 조회 실패");
}
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getAdapterBucketStatus(String adapterId) {
return getBucketStatus("adapter", adapterId, "어댑터가 로드되지 않음", "어댑터 버킷 상태 조회 실패");
}
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getInterfaceBucketStatus(String interfaceId) {
return getBucketStatus("interface", interfaceId, "인터페이스가 로드되지 않음", "인터페이스 버킷 상태 조회 실패");
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> getBucketStatus(String target, String targetId, String noDataMessage, String logPrefix) {
List<Map<String, Object>> result = new ArrayList<>();
List<Map<String, String>> serverList = eaiServerInfoService.getEaiServerIpList();
for (Map<String, String> server : serverList) {
String serverName = server.get("EAISVRINSTNM");
String serverIp = server.get("EAISVRIP");
String serverPort = server.get("WEBSERVERPORT");
if (StringUtils.isEmpty(serverPort)) {
serverPort = server.get("EAISVRLSNPORT");
}
Map<String, Object> serverStatus = new HashMap<>();
serverStatus.put("serverName", serverName);
serverStatus.put("serverIp", serverIp);
serverStatus.put("serverPort", serverPort);
try {
String url = String.format("http://%s:%s/manage/inflow/%s/%s/bucket-status",
serverIp, serverPort, target, targetId);
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
if (response.getBody() != null && Boolean.TRUE.equals(response.getBody().get("success"))) {
serverStatus.put("status", "online");
serverStatus.put("data", response.getBody().get("data"));
} else {
serverStatus.put("status", "no_data");
serverStatus.put("message", noDataMessage);
}
} catch (Exception e) {
logger.warn("{} - server: {}, error: {}", logPrefix, serverName, e.getMessage());
serverStatus.put("status", "offline");
serverStatus.put("message", "서버 연결 실패");
}
result.add(serverStatus);
}
return result;
}
private InflowControlId toId(InflowControlManUI ui) { private InflowControlId toId(InflowControlManUI ui) {
return toId(ui.getType(), ui.getName()); return toId(ui.getType(), ui.getName());
@@ -83,4 +83,12 @@ public class InflowInterfaceControlManController extends OnlBaseAnnotationContro
return new ModelAndView("jsonView", resultMap); return new ModelAndView("jsonView", resultMap);
} }
@RequestMapping(value = "/onl/admin/inflow/inflowInterfaceControlMan.json", params = "cmd=LIST_BUCKET_STATUS")
public ModelAndView getBucketStatus(HttpServletRequest request, HttpServletResponse response, String interfaceId) {
List<Map<String, Object>> statusList = service.getInterfaceBucketStatus(interfaceId);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("servers", statusList);
return new ModelAndView("jsonView", resultMap);
}
} }