Compare commits
41 Commits
a2cda26909
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bf7f661e3 | |||
| 124df00f51 | |||
| e4de8ebc8b | |||
| 52e8c3be39 | |||
| 136626e7c4 | |||
| 0848cd3469 | |||
| 0b6f273132 | |||
| 266644798f | |||
| 2e8b45c69e | |||
| ea3d23424c | |||
| 2a9dfaa093 | |||
| 3b74f9ce38 | |||
| 145ad37499 | |||
| a235b0656c | |||
| da397b21b0 | |||
| e084436c90 | |||
| 15652e158d | |||
| 8e4c2efe88 | |||
| cad51df3cb | |||
| d19a9eb593 | |||
| a05870fd1b | |||
| 47259da0f6 | |||
| 0bdcc440b7 | |||
| 778197c1dd | |||
| 91631ea750 | |||
| a1d7f8f10e | |||
| 1ba140ddea | |||
| 7117420f38 | |||
| 536151a737 | |||
| 1ac84f2f92 | |||
| 60d1dcea96 | |||
| bb3fb5e111 | |||
| 1608d29726 | |||
| d08c17bb13 | |||
| 5fd8334ea8 | |||
| a1d2058bca | |||
| 927ef830ac | |||
| 9cdfad1317 | |||
| bc40362c47 | |||
| 74f7aa5cf9 | |||
| 258bb70a93 |
@@ -1,7 +1,7 @@
|
|||||||
UserManController_사용자관리_APIGW_INSERT,UPDATE,DELETE
|
UserManController_사용자관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
AdapterController_어댑터관리_APIGW_INSERT,UPDATE,DELETE
|
AdapterController_어댑터관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
ApiInterfaceController_API관리_APIGW_INSERT,UPDATE,DELETE,CLONE
|
ApiInterfaceController_API관리_APIGW_INSERT,UPDATE,DELETE,CLONE
|
||||||
ClientController_Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE
|
ClientController_Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE,SYNC_PORTAL
|
||||||
RoleController_Role (역할)관리_APIGW_INSERT,UPDATE,DELETE
|
RoleController_Role (역할)관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
MenuController_Menu관리_APIGW_INSERT,UPDATE,DELETE
|
MenuController_Menu관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
PortalApprovalLineManController_승인라인관리_APIGW_INSERT,UPDATE,DELETE
|
PortalApprovalLineManController_승인라인관리_APIGW_INSERT,UPDATE,DELETE
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
</mvc:interceptor>
|
</mvc:interceptor>
|
||||||
<mvc:interceptor>
|
<mvc:interceptor>
|
||||||
<mvc:mapping path="/_onl/**"/>
|
<mvc:mapping path="/_onl/**"/>
|
||||||
<bean class="com.eactive.eai.rms.common.interceptor.ApiKeyInterceptor" />
|
<bean class="com.eactive.eai.rms.common.interceptor.InternalApiTokenInterceptor" />
|
||||||
</mvc:interceptor>
|
</mvc:interceptor>
|
||||||
<!-- BaseRestController 엔드포인트 IP 화이트리스트 인증 -->
|
<!-- BaseRestController 엔드포인트 IP 화이트리스트 인증 -->
|
||||||
<mvc:interceptor>
|
<mvc:interceptor>
|
||||||
|
|||||||
@@ -175,6 +175,6 @@
|
|||||||
</jsp-config>
|
</jsp-config>
|
||||||
|
|
||||||
<session-config>
|
<session-config>
|
||||||
<session-timeout>30</session-timeout>
|
<session-timeout>60</session-timeout>
|
||||||
</session-config>
|
</session-config>
|
||||||
</web-app>
|
</web-app>
|
||||||
@@ -145,6 +145,6 @@
|
|||||||
</jsp-config>
|
</jsp-config>
|
||||||
|
|
||||||
<session-config>
|
<session-config>
|
||||||
<session-timeout>30</session-timeout>
|
<session-timeout>60</session-timeout>
|
||||||
</session-config>
|
</session-config>
|
||||||
</web-app>
|
</web-app>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<context-root>monitoring</context-root>
|
<context-root>monitoring</context-root>
|
||||||
<session-descriptor>
|
<session-descriptor>
|
||||||
<timeout-secs>1800</timeout-secs>
|
<timeout-secs>3600</timeout-secs>
|
||||||
<cookie-name>JSESSIONID_EMS</cookie-name>
|
<cookie-name>JSESSIONID_EMS</cookie-name>
|
||||||
<persistent-store-type>replicated_if_clustered</persistent-store-type>
|
<persistent-store-type>replicated_if_clustered</persistent-store-type>
|
||||||
</session-descriptor>
|
</session-descriptor>
|
||||||
|
|||||||
@@ -29,6 +29,14 @@
|
|||||||
// (enricher 는 oauth·api_key 를 모두 apiKey-in-header 로 모델링하므로 scheme.type 만으로 구분 불가)
|
// (enricher 는 oauth·api_key 를 모두 apiKey-in-header 로 모델링하므로 scheme.type 만으로 구분 불가)
|
||||||
var djbAuthType = '';
|
var djbAuthType = '';
|
||||||
|
|
||||||
|
// 마지막 저장(또는 최초 로드) 시점의 state.data 스냅샷 — 닫기 전 미저장 변경 여부 판별용.
|
||||||
|
var savedSnapshot = null;
|
||||||
|
function isDirty() { return savedSnapshot !== null && JSON.stringify(state.data) !== savedSnapshot; }
|
||||||
|
function closeEditor() {
|
||||||
|
if (isDirty() && !window.confirm('저장하지 않은 변경사항이 있습니다. 저장하지 않고 닫으시겠습니까?')) return;
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 2. 유틸
|
// 2. 유틸
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -1703,7 +1711,7 @@
|
|||||||
|
|
||||||
var bSave = $('#btn-save'); if (bSave) bSave.addEventListener('click', () => saveSpec(false));
|
var bSave = $('#btn-save'); if (bSave) bSave.addEventListener('click', () => saveSpec(false));
|
||||||
var bRegen = $('#btn-regen'); if (bRegen) bRegen.addEventListener('click', regenerate);
|
var bRegen = $('#btn-regen'); if (bRegen) bRegen.addEventListener('click', regenerate);
|
||||||
var bClose = $('#btn-close'); if (bClose) bClose.addEventListener('click', () => window.close());
|
var bClose = $('#btn-close'); if (bClose) bClose.addEventListener('click', closeEditor);
|
||||||
|
|
||||||
// Cmd/Ctrl + S → 저장 (브라우저 기본 저장 대화상자 차단)
|
// Cmd/Ctrl + S → 저장 (브라우저 기본 저장 대화상자 차단)
|
||||||
document.addEventListener('keydown', function (e) {
|
document.addEventListener('keydown', function (e) {
|
||||||
@@ -2066,6 +2074,7 @@
|
|||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res && res.status === 'success') {
|
if (res && res.status === 'success') {
|
||||||
toast(temp ? '임시저장되었습니다' : '저장되었습니다');
|
toast(temp ? '임시저장되었습니다' : '저장되었습니다');
|
||||||
|
savedSnapshot = JSON.stringify(state.data); // 저장 성공 시점을 새 저장 기준선으로 갱신
|
||||||
var badge = $('#hdr-source'); if (badge) { badge.textContent = '저장본'; badge.className = 'ml-1 px-2 py-0.5 text-[11px] rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200'; badge.classList.remove('hidden'); }
|
var badge = $('#hdr-source'); if (badge) { badge.textContent = '저장본'; badge.className = 'ml-1 px-2 py-0.5 text-[11px] rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200'; badge.classList.remove('hidden'); }
|
||||||
} else toast('저장 실패: ' + (res && res.errorMsg || ''), 'error');
|
} else toast('저장 실패: ' + (res && res.errorMsg || ''), 'error');
|
||||||
})
|
})
|
||||||
@@ -2102,6 +2111,7 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
function init() {
|
function init() {
|
||||||
loadInitialData().then(function () {
|
loadInitialData().then(function () {
|
||||||
|
savedSnapshot = JSON.stringify(state.data); // 초기 로드(자동생성 포함) 완료 시점을 저장 기준선으로 설정
|
||||||
renderStepper();
|
renderStepper();
|
||||||
renderForm();
|
renderForm();
|
||||||
bindGlobalEvents();
|
bindGlobalEvents();
|
||||||
|
|||||||
@@ -7,6 +7,19 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||||
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
||||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||||
|
<%
|
||||||
|
// 비밀번호 규칙 안내문구용 모니터링 프로퍼티 조회
|
||||||
|
com.eactive.eai.rms.common.context.MonitoringContext monitoringContext =
|
||||||
|
(com.eactive.eai.rms.common.context.MonitoringContext) WebApplicationContextUtils
|
||||||
|
.getRequiredWebApplicationContext(pageContext.getServletContext())
|
||||||
|
.getBean("monitoringContext");
|
||||||
|
int pwdLengthCheck = monitoringContext.getIntProperty(
|
||||||
|
com.eactive.eai.rms.common.context.MonitoringContext.RMS_PASSWORD_LENGTH_CHECK, 7);
|
||||||
|
int pwdCombiCheck = monitoringContext.getIntProperty(
|
||||||
|
com.eactive.eai.rms.common.context.MonitoringContext.RMS_PASSWORD_COMBI_CHECK, 2);
|
||||||
|
int pwdRepeatCheck = monitoringContext.getIntProperty(
|
||||||
|
com.eactive.eai.rms.common.context.MonitoringContext.RMS_PASSWORD_REPEAT_CHECK, 3);
|
||||||
|
%>
|
||||||
<c:set var="themeColor" value="<%=System.getProperty(\"theme.color\")%>" scope="session" />
|
<c:set var="themeColor" value="<%=System.getProperty(\"theme.color\")%>" scope="session" />
|
||||||
|
|
||||||
<%
|
<%
|
||||||
@@ -92,6 +105,7 @@
|
|||||||
data: $('#loginForm').serialize(),
|
data: $('#loginForm').serialize(),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
|
console.log('res', response)
|
||||||
if (response.changePassword) {
|
if (response.changePassword) {
|
||||||
showChangeInitPassword(response);
|
showChangeInitPassword(response);
|
||||||
}
|
}
|
||||||
@@ -103,8 +117,14 @@
|
|||||||
// 직접 로그인 성공
|
// 직접 로그인 성공
|
||||||
window.location.href = response.redirectUrl;
|
window.location.href = response.redirectUrl;
|
||||||
} else {
|
} else {
|
||||||
// 로그인 실패
|
if (response.errorMessage == 'NO_PHONE_NUMBER') {
|
||||||
alert('로그인에 실패했습니다');
|
alert("휴대폰번호가 등록되어 있지 않아 로그인할 수 없습니다. 관리자에게 문의하세요")
|
||||||
|
} else if (response.errorMessage == 'FAIL_SEND_AUTH') {
|
||||||
|
alert("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.")
|
||||||
|
} else {
|
||||||
|
// 로그인 실패
|
||||||
|
alert('로그인에 실패했습니다');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
@@ -299,8 +319,12 @@
|
|||||||
if (response.smsAuthRequired) {
|
if (response.smsAuthRequired) {
|
||||||
// 비밀번호는 아직 반영되지 않음 - SMS 인증 완료 시 최종 반영됨
|
// 비밀번호는 아직 반영되지 않음 - SMS 인증 완료 시 최종 반영됨
|
||||||
smsAuthPurpose = 'PASSWORD_CHANGE';
|
smsAuthPurpose = 'PASSWORD_CHANGE';
|
||||||
|
// 두 모달이 겹치면 이전 모달의 enforceFocus/backdrop 이 남아 입력이 막히므로
|
||||||
|
// #pwdChgModal 이 완전히 닫힌 뒤 SMS 인증 모달을 연다
|
||||||
|
$('#pwdChgModal').one('hidden.bs.modal', function() {
|
||||||
|
showSmsAuthModal(response);
|
||||||
|
});
|
||||||
$('#pwdChgModal').modal('hide');
|
$('#pwdChgModal').modal('hide');
|
||||||
showSmsAuthModal(response);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
alert(response.message);
|
alert(response.message);
|
||||||
@@ -369,6 +393,19 @@
|
|||||||
verifySmsAuthCode();
|
verifySmsAuthCode();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 인증번호는 숫자만 허용 (타이핑/붙여넣기/드래그 공통, 백스페이스·방향키는 방해하지 않음)
|
||||||
|
$("#smsAuthCode").on("input", function(){
|
||||||
|
var digits = this.value.replace(/[^0-9]/g, '').slice(0, 6);
|
||||||
|
if (this.value !== digits) {
|
||||||
|
this.value = digits;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// SMS 인증 모달이 완전히 열린 뒤 인증번호 입력창에 포커스
|
||||||
|
$('#smsAuthModal').on('shown.bs.modal', function() {
|
||||||
|
$('#smsAuthCode').trigger('focus');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function fncSsoLogin() {
|
function fncSsoLogin() {
|
||||||
@@ -464,8 +501,8 @@
|
|||||||
<input type="password" name="confirmPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderConfirmationPassword") %>" autocomplete="off" required>
|
<input type="password" name="confirmPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderConfirmationPassword") %>" autocomplete="off" required>
|
||||||
</div>
|
</div>
|
||||||
<span style="font-size:0.8em; color:red">
|
<span style="font-size:0.8em; color:red">
|
||||||
※ 7글자 이상 & 영문/숫자/특수문자 2종류 이상<br/>
|
※ <%= pwdLengthCheck %>글자 이상 & 영문/숫자/특수문자 <%= pwdCombiCheck %>종류 이상<br/>
|
||||||
※ 연속된 숫자/문자(예: 123, abc, qwer), 동일 문자 3자 이상 반복 사용 불가
|
※ 연속된 숫자/문자(예: 123, abc, qwer) 사용 불가<%= pwdRepeatCheck >= 2 ? ", 동일 문자 " + pwdRepeatCheck + "자 이상 반복 불가" : "" %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -487,8 +524,7 @@
|
|||||||
<p>인증번호가 <strong><span id="smsAuthMaskedPhone"></span></strong>로 발송되었습니다.</p>
|
<p>인증번호가 <strong><span id="smsAuthMaskedPhone"></span></strong>로 발송되었습니다.</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" id="smsAuthCode" class="form-control"
|
<input type="text" id="smsAuthCode" class="form-control"
|
||||||
placeholder="인증번호 6자리" maxlength="6"
|
placeholder="인증번호 6자리" maxlength="6" inputmode="numeric" autocomplete="off">
|
||||||
onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
|
||||||
</div>
|
</div>
|
||||||
<p id="smsAuthTestHint" class="text-warning" style="display:none;"></p>
|
<p id="smsAuthTestHint" class="text-warning" style="display:none;"></p>
|
||||||
<p class="text-muted">남은 시간: <span id="smsAuthRemainingTime" class="text-danger font-weight-bold">60</span>초</p>
|
<p class="text-muted">남은 시간: <span id="smsAuthRemainingTime" class="text-danger font-weight-bold">60</span>초</p>
|
||||||
|
|||||||
@@ -596,7 +596,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li style="width:240px;">
|
<li style="width:240px;">
|
||||||
<a style="width:240px; cursor: default"
|
<a style="width:240px; cursor: default"
|
||||||
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %></span>
|
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>)</span>
|
||||||
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
|
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
|
||||||
<% if (!"".equals(LastLoginYms.trim())) { %>
|
<% if (!"".equals(LastLoginYms.trim())) { %>
|
||||||
<span style="display:block;font-size:10px;"><%=localeMessage.getString("screen.lastLogin") %> : <%=LastLoginYms%></span>
|
<span style="display:block;font-size:10px;"><%=localeMessage.getString("screen.lastLogin") %> : <%=LastLoginYms%></span>
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
viewrecords : true,
|
viewrecords : true,
|
||||||
gridview : true,
|
gridview : true,
|
||||||
ondblClickRow : function(rowId) {
|
ondblClickRow : function(rowId) {
|
||||||
console.log("rowId", rowId);
|
/* console.log("rowId", rowId);
|
||||||
var rowData = $(this)
|
var rowData = $(this)
|
||||||
.getRowData(rowId);
|
.getRowData(rowId);
|
||||||
var key = rowData['EaiSevrInstncName'];
|
var key = rowData['EaiSevrInstncName'];
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
url2 += "&searchAdptrBzwkGroupName="
|
url2 += "&searchAdptrBzwkGroupName="
|
||||||
+ key2;
|
+ key2;
|
||||||
url2 += '&menuId='+'${param.menuId}';
|
url2 += '&menuId='+'${param.menuId}';
|
||||||
goNav(url2);
|
goNav(url2); */
|
||||||
},
|
},
|
||||||
loadComplete : function(d) {
|
loadComplete : function(d) {
|
||||||
$("button[name*=img]").unbind("click");
|
$("button[name*=img]").unbind("click");
|
||||||
|
|||||||
@@ -190,6 +190,8 @@
|
|||||||
var key = "${param.clientId}";
|
var key = "${param.clientId}";
|
||||||
if (key != "" && key != "null") {
|
if (key != "" && key != "null") {
|
||||||
isDetail = true;
|
isDetail = true;
|
||||||
|
} else {
|
||||||
|
$('input[name="grantTypes_"]').prop('checked', true);
|
||||||
}
|
}
|
||||||
init(key, detail);
|
init(key, detail);
|
||||||
|
|
||||||
@@ -306,7 +308,8 @@
|
|||||||
dataType: "json",
|
dataType: "json",
|
||||||
data: {
|
data: {
|
||||||
cmd: 'SYNC_PORTAL',
|
cmd: 'SYNC_PORTAL',
|
||||||
clientId: clientId
|
clientId: clientId,
|
||||||
|
serviceType: 'APIGW'
|
||||||
},
|
},
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
showSyncResultModal(response);
|
showSyncResultModal(response);
|
||||||
@@ -451,8 +454,8 @@
|
|||||||
</div><!-- end content_top -->
|
</div><!-- end content_top -->
|
||||||
<div class="content_middle">
|
<div class="content_middle">
|
||||||
<div class="search_wrap">
|
<div class="search_wrap">
|
||||||
<!-- <button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
<button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
||||||
</button> -->
|
</button>
|
||||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %>
|
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i
|
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i
|
||||||
@@ -506,18 +509,17 @@
|
|||||||
<td><input type="text" name="scope"/></td>
|
<td><input type="text" name="scope"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%= localeMessage.getString("clntManDtl.grntTp") %>
|
<th><%= localeMessage.getString("clntManDtl.grntTp") %> <small>(*)</small></th>
|
||||||
(<%= localeMessage.getString("clntManDtl.sprt") %> ,) <small>(*)</small></th>
|
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" name="grantTypes_" value="client_credentials"> client_credentials
|
<input type="checkbox" name="grantTypes_" value="client_credentials"> client_credentials
|
||||||
<%-- <input type="checkbox" name="grantTypes_" value="authorization_code"> authorization_code--%>
|
<%-- <input type="checkbox" name="grantTypes_" value="authorization_code"> authorization_code--%>
|
||||||
<%-- <input type="checkbox" name="grantTypes_" value="password"> password--%>
|
<%-- <input type="checkbox" name="grantTypes_" value="password"> password--%>
|
||||||
<input type="checkbox" name="grantTypes_" value="refresh_token"> refresh_token
|
<!-- <input type="checkbox" name="grantTypes_" value="refresh_token"> refresh_token -->
|
||||||
</td>
|
</td>
|
||||||
<input type="hidden" name="grantTypes"/>
|
<input type="hidden" name="grantTypes"/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr style="display: none;">
|
||||||
<th>REDIRECT URI</th>
|
<th>REDIRECT URI</th>
|
||||||
<td><input type="text" name="redirectUri"/></td>
|
<td><input type="text" name="redirectUri"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -538,7 +540,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<td><input type="text" name="accessTokenValiditySeconds"/></td>
|
<td><input type="text" name="accessTokenValiditySeconds"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr style="display: none;">
|
||||||
<th><%= localeMessage.getString("clntManDtl.rfrshTknExprtnDt") %>
|
<th><%= localeMessage.getString("clntManDtl.rfrshTknExprtnDt") %>
|
||||||
</th>
|
</th>
|
||||||
<td><input type="text" name="refreshTokenValiditySeconds"/></td>
|
<td><input type="text" name="refreshTokenValiditySeconds"/></td>
|
||||||
@@ -555,7 +557,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<td><input type="text" name="allowedIps"/></td>
|
<td><input type="text" name="allowedIps"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr style="display: none;">
|
||||||
<th>Security Key</th>
|
<th>Security Key</th>
|
||||||
<td><textarea name="securityKey" rows="3" style="width:100%;"></textarea></td>
|
<td><textarea name="securityKey" rows="3" style="width:100%;"></textarea></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -740,7 +742,7 @@ function showSyncResultModal(response) {
|
|||||||
if (response.success) {
|
if (response.success) {
|
||||||
statusClass = "status-success";
|
statusClass = "status-success";
|
||||||
var createdCount = response.createdApiSpecCount || 0;
|
var createdCount = response.createdApiSpecCount || 0;
|
||||||
var createdMsg = createdCount > 0 ? "<br/><span style='color:#007bff;'>* " + createdCount + "개의 API Spec이 자동 생성되었습니다.</span>" : "";
|
var createdMsg = createdCount > 0 ? "<br/><span style='color:#007bff;'>* " + createdCount + "개의 API Spec을 비공개(N)로 자동 생성했습니다. API Spec 관리에서 확인 후 공개하세요.</span>" : "";
|
||||||
|
|
||||||
if (response.action === 'INSERTED') {
|
if (response.action === 'INSERTED') {
|
||||||
statusMsg = "<strong>반영 완료</strong> - 포탈에 새로운 인증정보가 생성되었습니다." + createdMsg;
|
statusMsg = "<strong>반영 완료</strong> - 포탈에 새로운 인증정보가 생성되었습니다." + createdMsg;
|
||||||
@@ -770,6 +772,7 @@ function showSyncResultModal(response) {
|
|||||||
var syncedApis = response.syncedApis || [];
|
var syncedApis = response.syncedApis || [];
|
||||||
var invalidApis = response.invalidApis || [];
|
var invalidApis = response.invalidApis || [];
|
||||||
var createdApiSpecs = response.createdApiSpecs || [];
|
var createdApiSpecs = response.createdApiSpecs || [];
|
||||||
|
var specGenFailed = response.specGenFailed || [];
|
||||||
|
|
||||||
$targetCount.text(targetApis.length);
|
$targetCount.text(targetApis.length);
|
||||||
|
|
||||||
@@ -783,8 +786,11 @@ function showSyncResultModal(response) {
|
|||||||
if (invalidApis.includes(apiId)) {
|
if (invalidApis.includes(apiId)) {
|
||||||
status = "미존재 (APIGW에 없음)";
|
status = "미존재 (APIGW에 없음)";
|
||||||
statusClass = "api-invalid";
|
statusClass = "api-invalid";
|
||||||
|
} else if (specGenFailed.includes(apiId)) {
|
||||||
|
status = "반영됨 (Spec 자동생성 실패·빈 항목)";
|
||||||
|
statusClass = "api-created";
|
||||||
} else if (createdApiSpecs.includes(apiId)) {
|
} else if (createdApiSpecs.includes(apiId)) {
|
||||||
status = "반영됨 (Spec 신규생성)";
|
status = "반영됨 (Spec 자동생성·비공개)";
|
||||||
statusClass = "api-created";
|
statusClass = "api-created";
|
||||||
} else if (syncedApis.includes(apiId)) {
|
} else if (syncedApis.includes(apiId)) {
|
||||||
status = "반영됨";
|
status = "반영됨";
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||||
colNames: ['No', 'Service Name', 'Command', '레이아웃명', '연동시간', '결과', '결과메세지'],
|
colNames: ['No', '서비스명', 'Command', '연동구분명', '연동시간', '결과', '결과메세지'],
|
||||||
colModel: [
|
colModel: [
|
||||||
{name: 'seqno', align: 'center', width: 50, sortable: false},
|
{name: 'seqno', align: 'center', width: 50, sortable: false},
|
||||||
{name: 'serviceName', align: 'center', width: 100, sortable: false},
|
{name: 'serviceName', align: 'center', width: 100, sortable: false},
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
<div class="search_wrap">
|
<div class="search_wrap">
|
||||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="title">전문레이아웃 연동 이력<span class="tooltip">IIM에서 연동한 인터페이스 및 전문 레이아웃 이력을 조회합니다.</span></div>
|
<div class="title">연동 배포 이력<span class="tooltip">IIM 레이아웃 연동 이력과 eCams 인터페이스 배포 이력을 조회합니다.</span></div>
|
||||||
<form id="ajaxForm" onsubmit="return false;">
|
<form id="ajaxForm" onsubmit="return false;">
|
||||||
<table class="search_condition" cellspacing=0;>
|
<table class="search_condition" cellspacing=0;>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
@@ -133,8 +133,6 @@
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>레이아웃명</th>
|
|
||||||
<td><input type="text" name="searchLoutName" autocomplete="off" style="width:95%;"></td>
|
|
||||||
<th>연동시간</th>
|
<th>연동시간</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:100px; border:1px solid #ebebec;">
|
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:100px; border:1px solid #ebebec;">
|
||||||
@@ -143,6 +141,14 @@
|
|||||||
<input type="hidden" name="searchStartDate" value="">
|
<input type="hidden" name="searchStartDate" value="">
|
||||||
<input type="hidden" name="searchEndDate" value="">
|
<input type="hidden" name="searchEndDate" value="">
|
||||||
</th>
|
</th>
|
||||||
|
<th>서비스명</th>
|
||||||
|
<td><input type="text" name="searchServiceName" autocomplete="off" style="width:95%;"></td>
|
||||||
|
<th>Command</th>
|
||||||
|
<td><input type="text" name="searchCommand" autocomplete="off" style="width:95%;"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>연동구분명</th>
|
||||||
|
<td><input type="text" name="searchLoutName" autocomplete="off" style="width:95%;"></td>
|
||||||
<th>결과</th>
|
<th>결과</th>
|
||||||
<td>
|
<td>
|
||||||
<div class="select-style" style="display:inline-block; margin-left:6px;">
|
<div class="select-style" style="display:inline-block; margin-left:6px;">
|
||||||
@@ -153,6 +159,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<th></th>
|
||||||
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
<div class="search_wrap">
|
<div class="search_wrap">
|
||||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
<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" id="title">전문레이아웃 연동 이력 상세</div>
|
<div class="title" id="title">연동 배포 이력 상세</div>
|
||||||
<table class="table_row" cellspacing="0">
|
<table class="table_row" cellspacing="0">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style="width:12%"/><col style="width:38%"/>
|
<col style="width:12%"/><col style="width:38%"/>
|
||||||
|
|||||||
@@ -47,6 +47,8 @@
|
|||||||
$('#sentDate').text(data.sentDateFull || '');
|
$('#sentDate').text(data.sentDateFull || '');
|
||||||
// 서버에서 마스킹+escape 된 안전 HTML
|
// 서버에서 마스킹+escape 된 안전 HTML
|
||||||
$('#messageHtml').html(data.messageHtml || '');
|
$('#messageHtml').html(data.messageHtml || '');
|
||||||
|
// 응답내용(RESPONSE_DATA) — 원문이므로 text 로만 출력
|
||||||
|
$('#responseData').text(data.responseData || '');
|
||||||
|
|
||||||
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
|
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
|
||||||
if (data.requestStatus !== 'PENDING') {
|
if (data.requestStatus !== 'PENDING') {
|
||||||
@@ -142,6 +144,10 @@
|
|||||||
<th>메세지 내용</th>
|
<th>메세지 내용</th>
|
||||||
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
|
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>응답내용</th>
|
||||||
|
<td colspan="3"><div id="responseData" class="msg-detail"></div></td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -118,6 +118,9 @@
|
|||||||
$('.incident-endat-hint').hide();
|
$('.incident-endat-hint').hide();
|
||||||
$('#endAt').prop('readonly', false);
|
$('#endAt').prop('readonly', false);
|
||||||
}
|
}
|
||||||
|
// 양식(템플릿)은 장애 공지에만 쓴다 - 점검은 문구 체계가 다르다
|
||||||
|
$('.template-row').toggle(t === NOTICE_TYPE_INCIDENT);
|
||||||
|
|
||||||
// 타임라인은 등록된 장애(수정 모드)에서만 다룬다
|
// 타임라인은 등록된 장애(수정 모드)에서만 다룬다
|
||||||
if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) {
|
if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) {
|
||||||
$('#timelineSection').show();
|
$('#timelineSection').show();
|
||||||
@@ -126,6 +129,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 장애 공지 양식(제목·본문)을 서버에서 받아 채운다.
|
||||||
|
* 문구는 PTL_PROPERTY 'djb.apistatus.draft.*' 이며 자동 탐지 초안과 같은 양식이다.
|
||||||
|
*
|
||||||
|
* @param force true 면 이미 입력한 제목·본문도 (확인 후) 덮어쓴다
|
||||||
|
*/
|
||||||
|
function applyTemplate(force) {
|
||||||
|
if (force) {
|
||||||
|
var hasInput = $.trim($('#noticeSubject').val()) !== '' || !$('#contents').summernote('isEmpty');
|
||||||
|
if (hasInput && !confirm('현재 입력한 제목·본문을 양식으로 덮어씁니다. 계속하시겠습니까?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: "POST", url: url, dataType: "json",
|
||||||
|
data: {cmd: 'DRAFT_TEMPLATE', apisJson: JSON.stringify(affectedApis)},
|
||||||
|
success: function (data) {
|
||||||
|
if (!data) return;
|
||||||
|
$('#noticeSubject').val(data.subject || '');
|
||||||
|
$('#contents').summernote('code', data.detail || '');
|
||||||
|
},
|
||||||
|
error: function (e) { alert(e.responseText); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function decodeHTMLEntities(text) {
|
function decodeHTMLEntities(text) {
|
||||||
var textArea = document.createElement('textarea');
|
var textArea = document.createElement('textarea');
|
||||||
textArea.innerHTML = text;
|
textArea.innerHTML = text;
|
||||||
@@ -441,6 +469,15 @@
|
|||||||
|
|
||||||
$('#noticeType').on('change', toggleIncidentFields);
|
$('#noticeType').on('change', toggleIncidentFields);
|
||||||
|
|
||||||
|
// 신규 등록에서 장애를 고르면 빈 본문에 한해 양식을 자동으로 채운다.
|
||||||
|
// (수정 모드나 이미 쓴 본문은 건드리지 않는다 - 덮어쓰려면 '양식 적용' 버튼)
|
||||||
|
$('#noticeType').on('change', function () {
|
||||||
|
if (!isDetail && $(this).val() === NOTICE_TYPE_INCIDENT && $('#contents').summernote('isEmpty')) {
|
||||||
|
applyTemplate(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#btn_applyTemplate').click(function () { applyTemplate(true); });
|
||||||
|
|
||||||
$('#btn_addAffectedApi').click(openApiPopup);
|
$('#btn_addAffectedApi').click(openApiPopup);
|
||||||
$('#btn_removeAffectedApi').click(removeSelectedAffectedApis);
|
$('#btn_removeAffectedApi').click(removeSelectedAffectedApis);
|
||||||
|
|
||||||
@@ -493,6 +530,10 @@
|
|||||||
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) return;
|
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) return;
|
||||||
|
|
||||||
var formData = new FormData($("#ajaxForm")[0]);
|
var formData = new FormData($("#ajaxForm")[0]);
|
||||||
|
// Summernote 그림 다이얼로그의 내부 파일 input(name="files")이 #ajaxForm 안에 있어
|
||||||
|
// 선택한 이미지가 지워지지 않고 남아 있으면 여기서 함께 딸려 온다.
|
||||||
|
// 본 화면은 첨부파일 기능을 쓰지 않으므로 항상 제거한다.
|
||||||
|
formData.delete("files");
|
||||||
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
|
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
|
||||||
formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N");
|
formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N");
|
||||||
formData.set("noticeDetail", $('#contents').summernote('code'));
|
formData.set("noticeDetail", $('#contents').summernote('code'));
|
||||||
@@ -590,7 +631,12 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>본문 <font color="red">*</font></th>
|
<th>본문 <font color="red">*</font>
|
||||||
|
<div class="template-row" style="display:none;margin-top:5px;">
|
||||||
|
<button type="button" class="cssbtn smallBtn" id="btn_applyTemplate" level="W" status="DETAIL,NEW"
|
||||||
|
style="min-width:0;width:90%;" title="장애 공지 양식으로 제목과 본문을 채웁니다.">양식 적용</button>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<td colspan="3">
|
<td colspan="3">
|
||||||
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
|
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -33,6 +33,18 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
/* 달력 팝업이 파이차트 가운데 숫자(z-index:10) 위로 오도록 */
|
||||||
|
#ui-datepicker-div {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
|
.cssbtn.small {
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 2px;
|
||||||
|
min-width: 24px;
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: #ebebec;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
||||||
@@ -58,6 +70,7 @@
|
|||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
|
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||||
title: {
|
title: {
|
||||||
text: '총 건수 분포',
|
text: '총 건수 분포',
|
||||||
left: 'center',
|
left: 'center',
|
||||||
@@ -99,7 +112,7 @@
|
|||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
]
|
]
|
||||||
@@ -286,6 +299,28 @@
|
|||||||
fetchChartData();
|
fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 조회기간을 시작일 기준 이전/다음 달의 1일~말일로 변경 후 조회 (direction: -1 이전달, 1 다음달)
|
||||||
|
function shiftMonthRange(direction) {
|
||||||
|
var v = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
|
||||||
|
if (!v || v.length < 6) return;
|
||||||
|
|
||||||
|
var year = parseInt(v.substring(0, 4));
|
||||||
|
var month = parseInt(v.substring(4, 6)) - 1; // 0-based
|
||||||
|
|
||||||
|
var first = new Date(year, month + direction, 1);
|
||||||
|
var last = new Date(year, month + direction + 1, 0); // 해당 달의 말일
|
||||||
|
|
||||||
|
function pad(n) { return String(n).padStart(2, '0'); }
|
||||||
|
function fmt(d) {
|
||||||
|
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
$("input[name=searchStartDateTime]").val(fmt(first));
|
||||||
|
$("input[name=searchEndDateTime]").val(fmt(last));
|
||||||
|
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
function exportToExcel(isSummary) {
|
function exportToExcel(isSummary) {
|
||||||
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
|
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
|
||||||
console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
|
console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
|
||||||
@@ -416,12 +451,13 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API ID', 'API명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'apiName', align: 'left', width: '250', sortable: false },
|
{ name: 'apiName', align: 'left', width: '150', sortable: false },
|
||||||
|
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
|
||||||
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
@@ -458,7 +494,7 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
@@ -505,6 +541,8 @@
|
|||||||
$("input[name=searchStartDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
|
$("input[name=searchStartDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||||
$("input[name=searchEndDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
|
$("input[name=searchEndDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||||
|
|
||||||
|
$("input[name=searchStartDateTime], input[name=searchEndDateTime]").datepicker();
|
||||||
|
|
||||||
// 기본값 설정 (어제 기준 해당 월 1일 ~ 어제)
|
// 기본값 설정 (어제 기준 해당 월 1일 ~ 어제)
|
||||||
var today = getToday();
|
var today = getToday();
|
||||||
var todayStr = today.replace(/-/g, '');
|
var todayStr = today.replace(/-/g, '');
|
||||||
@@ -544,6 +582,14 @@
|
|||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btnPrevMonth").click(function() {
|
||||||
|
shiftMonthRange(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnNextMonth").click(function() {
|
||||||
|
shiftMonthRange(1);
|
||||||
|
});
|
||||||
|
|
||||||
$("#btn_excel_export_summary").click(function() {
|
$("#btn_excel_export_summary").click(function() {
|
||||||
exportToExcel(true);
|
exportToExcel(true);
|
||||||
});
|
});
|
||||||
@@ -598,11 +644,13 @@
|
|||||||
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;">
|
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;">
|
||||||
~
|
~
|
||||||
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;">
|
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;">
|
||||||
|
<button type="button" id="btnPrevMonth" class="cssbtn small"><</button>
|
||||||
|
<button type="button" id="btnNextMonth" class="cssbtn small">></button>
|
||||||
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
|
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>API명</th>
|
<th>API ID</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||||
</td>
|
</td>
|
||||||
@@ -643,7 +691,7 @@
|
|||||||
<!-- 요약 그리드 -->
|
<!-- 요약 그리드 -->
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<div class="title" style="margin: 0;">API명별 요약 통계</div>
|
<div class="title" style="margin: 0;">API별 요약 통계</div>
|
||||||
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
||||||
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -33,6 +33,18 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
/* 달력 팝업이 파이차트 가운데 숫자(z-index:10) 위로 오도록 */
|
||||||
|
#ui-datepicker-div {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
|
.cssbtn.small {
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 2px;
|
||||||
|
min-width: 24px;
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: #ebebec;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
||||||
@@ -40,6 +52,8 @@
|
|||||||
var url = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.json"/>';
|
var url = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.json"/>';
|
||||||
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>';
|
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>';
|
||||||
var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
|
var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
|
||||||
|
var url_left_menu = '<c:url value="/leftMenu.do"/>';
|
||||||
|
var DASHBOARD_MENU_ID = '0305001'; // apiStatsMinuteMan(대시보드) 메뉴 ID
|
||||||
|
|
||||||
var totalDonutChart, callChart;
|
var totalDonutChart, callChart;
|
||||||
|
|
||||||
@@ -59,6 +73,7 @@
|
|||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
|
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||||
title: {
|
title: {
|
||||||
text: '총 건수 분포',
|
text: '총 건수 분포',
|
||||||
left: 'center',
|
left: 'center',
|
||||||
@@ -100,7 +115,7 @@
|
|||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
]
|
]
|
||||||
@@ -149,23 +164,20 @@
|
|||||||
var hour = parseInt(statTime.substring(8, 10));
|
var hour = parseInt(statTime.substring(8, 10));
|
||||||
|
|
||||||
var startDate = year + '-' + month + '-' + day;
|
var startDate = year + '-' + month + '-' + day;
|
||||||
var startTime = (hour < 10 ? '0' + hour : hour) + ':00';
|
var hourStr = (hour < 10 ? '0' + hour : hour);
|
||||||
|
var startTime = hourStr + ':00';
|
||||||
// 1시간 후 계산 (23시인 경우 다음날 00시로 자동 계산)
|
// 해당 시간대 끝(정시~59분)으로 이동 (예: 10:00~10:59)
|
||||||
var d = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), hour, 0, 0);
|
var endTime = hourStr + ':59';
|
||||||
d.setHours(d.getHours() + 1);
|
|
||||||
|
|
||||||
var endDate = d.getFullYear() + '-' +
|
|
||||||
(d.getMonth() + 1 < 10 ? '0' : '') + (d.getMonth() + 1) + '-' +
|
|
||||||
(d.getDate() < 10 ? '0' : '') + d.getDate();
|
|
||||||
var endTime = (d.getHours() < 10 ? '0' : '') + d.getHours() + ':00';
|
|
||||||
|
|
||||||
var params = '?searchStartDate=' + startDate + '&searchStartTime=' + startTime
|
var params = '?searchStartDate=' + startDate + '&searchStartTime=' + startTime
|
||||||
+ '&searchEndDate=' + endDate + '&searchEndTime=' + endTime;
|
+ '&searchEndDate=' + startDate + '&searchEndTime=' + endTime;
|
||||||
params += '&menuId='+'${param.menuId}';
|
params += '&menuId='+DASHBOARD_MENU_ID;
|
||||||
params += '&cmd='+'LIST';
|
params += '&cmd='+'LIST';
|
||||||
params += '&serviceType='+'APIGW';
|
params += '&serviceType='+'APIGW';
|
||||||
|
|
||||||
|
// 왼쪽 메뉴도 대시보드(분단위 통계) 메뉴로 전환
|
||||||
|
parent.leftFrame.location.href = url_left_menu + '?menuId=' + DASHBOARD_MENU_ID
|
||||||
|
+ '&serviceType=' + sessionStorage["serviceType"];
|
||||||
location.href = url_minute_view + params;
|
location.href = url_minute_view + params;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +304,25 @@
|
|||||||
fetchChartData();
|
fetchChartData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 조회일자를 하루 이동 후 조회 (direction: -1 이전날, 1 이후날)
|
||||||
|
function shiftSearchDate(direction) {
|
||||||
|
var v = $("input[name=searchDate]").val().replace(/-/g, "");
|
||||||
|
if (!v || v.length !== 8) return;
|
||||||
|
|
||||||
|
var d = new Date(
|
||||||
|
parseInt(v.substring(0, 4)),
|
||||||
|
parseInt(v.substring(4, 6)) - 1,
|
||||||
|
parseInt(v.substring(6, 8))
|
||||||
|
);
|
||||||
|
d.setDate(d.getDate() + direction);
|
||||||
|
|
||||||
|
function pad(n) { return String(n).padStart(2, '0'); }
|
||||||
|
$("input[name=searchDate]").val(
|
||||||
|
d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()));
|
||||||
|
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
function exportToExcel(isSummary) {
|
function exportToExcel(isSummary) {
|
||||||
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
|
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
|
||||||
console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
|
console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
|
||||||
@@ -418,12 +449,13 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API ID', 'API 명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'apiName', align: 'left', width: '250', sortable: false },
|
{ name: 'apiName', align: 'left', width: '150', sortable: false },
|
||||||
|
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
|
||||||
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
@@ -460,7 +492,7 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
@@ -506,6 +538,8 @@
|
|||||||
// 날짜 입력 마스크
|
// 날짜 입력 마스크
|
||||||
$("input[name=searchDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
$("input[name=searchDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||||
|
|
||||||
|
$("input[name=searchDate]").datepicker();
|
||||||
|
|
||||||
// 기본값 설정 (오늘)
|
// 기본값 설정 (오늘)
|
||||||
var today = getToday();
|
var today = getToday();
|
||||||
if (!$("input[name=searchDate]").val()) {
|
if (!$("input[name=searchDate]").val()) {
|
||||||
@@ -524,6 +558,14 @@
|
|||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btnPrevDate").click(function() {
|
||||||
|
shiftSearchDate(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnNextDate").click(function() {
|
||||||
|
shiftSearchDate(1);
|
||||||
|
});
|
||||||
|
|
||||||
$("#btn_excel_export_summary").click(function() {
|
$("#btn_excel_export_summary").click(function() {
|
||||||
exportToExcel(true);
|
exportToExcel(true);
|
||||||
});
|
});
|
||||||
@@ -576,11 +618,13 @@
|
|||||||
<th>조회일자</th>
|
<th>조회일자</th>
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
<input type="text" name="searchDate" value="${param.searchDate}" style="width:100px;">
|
<input type="text" name="searchDate" value="${param.searchDate}" style="width:100px;">
|
||||||
|
<button type="button" id="btnPrevDate" class="cssbtn small"><</button>
|
||||||
|
<button type="button" id="btnNextDate" class="cssbtn small">></button>
|
||||||
<span style="color:#888; font-size:12px; margin-left:10px;">(선택일 00시 ~ 23시 조회)</span>
|
<span style="color:#888; font-size:12px; margin-left:10px;">(선택일 00시 ~ 23시 조회)</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>API명</th>
|
<th>API ID</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||||
</td>
|
</td>
|
||||||
@@ -621,7 +665,7 @@
|
|||||||
<!-- 요약 그리드 -->
|
<!-- 요약 그리드 -->
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<div class="title" style="margin: 0;">API명별 요약 통계</div>
|
<div class="title" style="margin: 0;">API별 요약 통계</div>
|
||||||
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
||||||
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -26,6 +26,18 @@
|
|||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
.cssbtn.small {
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 2px;
|
||||||
|
min-width: 24px;
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: #ebebec;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
/* 달력 팝업이 파이차트 가운데 숫자(z-index:10) 위로 오도록 */
|
||||||
|
#ui-datepicker-div {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
||||||
@@ -51,6 +63,9 @@
|
|||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
|
// updateCharts()에서 series[].data를 itemStyle 없이 재설정하면 조각별 itemStyle.color가
|
||||||
|
// 병합 과정에서 유실되므로, 옵션 레벨 color 팔레트(성공/Timeout/시스템오류 순)로 색을 고정한다.
|
||||||
|
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||||
title: {
|
title: {
|
||||||
text: '총 건수 분포',
|
text: '총 건수 분포',
|
||||||
left: 'center',
|
left: 'center',
|
||||||
@@ -92,7 +107,7 @@
|
|||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
]
|
]
|
||||||
@@ -203,7 +218,6 @@
|
|||||||
function validateAndAdjustDateRange() {
|
function validateAndAdjustDateRange() {
|
||||||
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
|
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
|
||||||
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, "");
|
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, "");
|
||||||
var endDate = $("input[name=searchEndDate]").val().replace(/-/g, "");
|
|
||||||
var endTime = $("input[name=searchEndTime]").val().replace(/:/g, "");
|
var endTime = $("input[name=searchEndTime]").val().replace(/:/g, "");
|
||||||
|
|
||||||
if (!startDate || !startTime) return null;
|
if (!startDate || !startTime) return null;
|
||||||
@@ -219,35 +233,43 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI 업데이트 헬퍼
|
// 종료일시(yyyyMMddHHmm) 포맷 헬퍼
|
||||||
function updateEndDateTime(dt) {
|
function formatDateTime(dt) {
|
||||||
var d = dt.getFullYear() +
|
return dt.getFullYear() +
|
||||||
String(dt.getMonth() + 1).padStart(2, '0') +
|
String(dt.getMonth() + 1).padStart(2, '0') +
|
||||||
String(dt.getDate()).padStart(2, '0');
|
String(dt.getDate()).padStart(2, '0') +
|
||||||
var t = String(dt.getHours()).padStart(2, '0') +
|
String(dt.getHours()).padStart(2, '0') +
|
||||||
String(dt.getMinutes()).padStart(2, '0');
|
String(dt.getMinutes()).padStart(2, '0');
|
||||||
$("input[name=searchEndDate]").val(d.substring(0, 4) + '-' + d.substring(4, 6) + '-' + d.substring(6, 8));
|
}
|
||||||
$("input[name=searchEndTime]").val(t.substring(0, 2) + ':' + t.substring(2, 4));
|
|
||||||
return d + t;
|
// 종료시각 UI 업데이트 헬퍼
|
||||||
|
function updateEndTime(dt) {
|
||||||
|
$("input[name=searchEndTime]").val(
|
||||||
|
String(dt.getHours()).padStart(2, '0') + ':' + String(dt.getMinutes()).padStart(2, '0'));
|
||||||
|
return formatDateTime(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
var start = parseDateTime(startDate, startTime);
|
var start = parseDateTime(startDate, startTime);
|
||||||
var startDateTime = startDate + startTime;
|
var startDateTime = startDate + startTime;
|
||||||
var endDateTime;
|
var endDateTime;
|
||||||
|
|
||||||
if (endDate && endTime) {
|
if (endTime) {
|
||||||
var end = parseDateTime(endDate, endTime);
|
// 종료일은 시작일 기준. 종료시각이 시작시각보다 이르면 자정을 넘긴 것으로 보고 +1일
|
||||||
|
var end = parseDateTime(startDate, endTime);
|
||||||
|
if (end < start) {
|
||||||
|
end = new Date(end.getTime() + 24 * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
var diffMinutes = (end - start) / (1000 * 60);
|
var diffMinutes = (end - start) / (1000 * 60);
|
||||||
|
|
||||||
if (diffMinutes > 60) {
|
if (diffMinutes > 60) {
|
||||||
// 1시간 초과 시 시작 + 1시간으로 조정
|
// 1시간 초과 시 시작시각 기준 1시간치(정시~59분)로 조정
|
||||||
endDateTime = updateEndDateTime(new Date(start.getTime() + 60 * 60 * 1000));
|
endDateTime = updateEndTime(new Date(start.getTime() + 59 * 60 * 1000));
|
||||||
} else {
|
} else {
|
||||||
endDateTime = endDate + endTime;
|
endDateTime = formatDateTime(end);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 종료시간 미입력 시 시작 + 1시간으로 설정
|
// 종료시간 미입력 시 시작시각 기준 1시간치(정시~59분)로 설정
|
||||||
endDateTime = updateEndDateTime(new Date(start.getTime() + 60 * 60 * 1000));
|
endDateTime = updateEndTime(new Date(start.getTime() + 59 * 60 * 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { start: startDateTime, end: endDateTime };
|
return { start: startDateTime, end: endDateTime };
|
||||||
@@ -302,6 +324,45 @@
|
|||||||
fetchChartData(range);
|
fetchChartData(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 조회기간을 정시 단위 1시간 구간으로 이동 (direction: -1 이전, 1 이후)
|
||||||
|
// ex) 시작 08:35, 이전 클릭 -> 08:00~08:59, 다시 -> 07:00~07:59
|
||||||
|
// ex) 시작 08:35, 이후 클릭 -> 09:00~09:59, 다시 -> 10:00~10:59
|
||||||
|
function shiftTimeRange(direction) {
|
||||||
|
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
|
||||||
|
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, "");
|
||||||
|
if (!startDate || !startTime) return;
|
||||||
|
|
||||||
|
var start = new Date(
|
||||||
|
parseInt(startDate.substring(0, 4)),
|
||||||
|
parseInt(startDate.substring(4, 6)) - 1,
|
||||||
|
parseInt(startDate.substring(6, 8)),
|
||||||
|
parseInt(startTime.substring(0, 2)),
|
||||||
|
parseInt(startTime.substring(2, 4))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (direction < 0) {
|
||||||
|
// 분 단위가 있으면 정시로 내림, 이미 정시면 1시간 전
|
||||||
|
if (start.getMinutes() === 0) start.setHours(start.getHours() - 1);
|
||||||
|
else start.setMinutes(0);
|
||||||
|
} else {
|
||||||
|
// 분 단위가 있으면 다음 정시로 올림, 이미 정시면 1시간 후
|
||||||
|
start.setMinutes(0);
|
||||||
|
start.setHours(start.getHours() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 종료시각은 같은 시간대의 59분으로 설정 (예: 10:00~10:59)
|
||||||
|
var end = new Date(start.getTime() + 59 * 60 * 1000);
|
||||||
|
|
||||||
|
function pad(n) { return String(n).padStart(2, '0'); }
|
||||||
|
|
||||||
|
$("input[name=searchStartDate]").val(
|
||||||
|
start.getFullYear() + '-' + pad(start.getMonth() + 1) + '-' + pad(start.getDate()));
|
||||||
|
$("input[name=searchStartTime]").val(pad(start.getHours()) + ':' + pad(start.getMinutes()));
|
||||||
|
$("input[name=searchEndTime]").val(pad(end.getHours()) + ':' + pad(end.getMinutes()));
|
||||||
|
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
function exportToExcel(isSummary) {
|
function exportToExcel(isSummary) {
|
||||||
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
|
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
|
||||||
console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
|
console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
|
||||||
@@ -317,13 +378,10 @@
|
|||||||
serviceType: '${param.serviceType}'
|
serviceType: '${param.serviceType}'
|
||||||
};
|
};
|
||||||
|
|
||||||
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
|
var range = validateAndAdjustDateRange();
|
||||||
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, "");
|
if (range) {
|
||||||
var endDate = $("input[name=searchEndDate]").val().replace(/-/g, "");
|
postData.searchStartDateTime = range.start;
|
||||||
var endTime = $("input[name=searchEndTime]").val().replace(/:/g, "");
|
postData.searchEndDateTime = range.end;
|
||||||
if (startDate && startTime) {
|
|
||||||
postData.searchStartDateTime = startDate + startTime;
|
|
||||||
postData.searchEndDateTime = endDate + endTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Excel Export] Request data:', postData);
|
console.log('[Excel Export] Request data:', postData);
|
||||||
@@ -431,12 +489,13 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API ID', 'API 명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'apiName', align: 'left', width: '250', sortable: false },
|
{ name: 'apiName', align: 'left', width: '150', sortable: false },
|
||||||
|
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
|
||||||
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
@@ -473,7 +532,7 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
@@ -517,12 +576,14 @@
|
|||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// 날짜/시간 입력 마스크
|
// 날짜/시간 입력 마스크
|
||||||
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
$("input[name=searchStartDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||||
$("input[name=searchStartTime], input[name=searchEndTime]").inputmask("99:99", { 'autoUnmask': true });
|
$("input[name=searchStartTime], input[name=searchEndTime]").inputmask("99:99", { 'autoUnmask': true });
|
||||||
|
|
||||||
// 기본값 설정 (현재 시간(분) 기준 1시간 전 ~ 현재 시간(분))
|
$("input[name=searchStartDate]").datepicker();
|
||||||
|
|
||||||
|
// 기본값 설정 (현재 시간(분) 기준 59분 전 ~ 현재 시간(분))
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
var oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
var oneHourAgo = new Date(now.getTime() - 59 * 60 * 1000);
|
||||||
|
|
||||||
function formatDate(d) {
|
function formatDate(d) {
|
||||||
return d.getFullYear() + '-' +
|
return d.getFullYear() + '-' +
|
||||||
@@ -537,8 +598,7 @@
|
|||||||
$("input[name=searchStartDate]").val(formatDate(oneHourAgo));
|
$("input[name=searchStartDate]").val(formatDate(oneHourAgo));
|
||||||
$("input[name=searchStartTime]").val(formatTime(oneHourAgo));
|
$("input[name=searchStartTime]").val(formatTime(oneHourAgo));
|
||||||
}
|
}
|
||||||
if (!$("input[name=searchEndDate]").val()) {
|
if (!$("input[name=searchEndTime]").val()) {
|
||||||
$("input[name=searchEndDate]").val(formatDate(now));
|
|
||||||
$("input[name=searchEndTime]").val(formatTime(now));
|
$("input[name=searchEndTime]").val(formatTime(now));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,6 +614,14 @@
|
|||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btnPrevTime").click(function() {
|
||||||
|
shiftTimeRange(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnNextTime").click(function() {
|
||||||
|
shiftTimeRange(1);
|
||||||
|
});
|
||||||
|
|
||||||
$("#btn_excel_export_summary").click(function() {
|
$("#btn_excel_export_summary").click(function() {
|
||||||
exportToExcel(true);
|
exportToExcel(true);
|
||||||
});
|
});
|
||||||
@@ -568,8 +636,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 종료일/시간 변경 시 1시간 초과 검증
|
// 종료시각 변경 시 1시간 초과 검증
|
||||||
$("input[name=searchEndDate], input[name=searchEndTime]").on('change blur', function() {
|
$("input[name=searchEndTime]").on('change blur', function() {
|
||||||
validateAndAdjustDateRange();
|
validateAndAdjustDateRange();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -602,14 +670,15 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">조회기간</th>
|
<th style="width:100px;">조회기간</th>
|
||||||
<td style="width:400px">
|
<td style="width:400px">
|
||||||
<input type="text" name="searchStartDate" value="${param.searchStartDate}" style="width:100px;">
|
<input type="text" name="searchStartDate" value="${param.searchStartDate}" style="width:80px;">
|
||||||
<input type="text" name="searchStartTime" value="${param.searchStartTime}" style="width:60px;">
|
<input type="text" name="searchStartTime" value="${param.searchStartTime}" style="width:40px;">
|
||||||
~
|
~
|
||||||
<input type="text" name="searchEndDate" value="${param.searchEndDate}" style="width:100px;">
|
<input type="text" name="searchEndTime" value="${param.searchEndTime}" style="width:40px;">
|
||||||
<input type="text" name="searchEndTime" value="${param.searchEndTime}" style="width:60px;">
|
<button type="button" id="btnPrevTime" class="cssbtn small"><</button>
|
||||||
|
<button type="button" id="btnNextTime" class="cssbtn small">></button>
|
||||||
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span>
|
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span>
|
||||||
</td>
|
</td>
|
||||||
<th style="width:100px;">API명</th>
|
<th style="width:100px;">API ID</th>
|
||||||
<td style="width:400px">
|
<td style="width:400px">
|
||||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
|
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||||
title: {
|
title: {
|
||||||
text: '총 건수 분포',
|
text: '총 건수 분포',
|
||||||
left: 'center',
|
left: 'center',
|
||||||
@@ -99,7 +100,7 @@
|
|||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
]
|
]
|
||||||
@@ -421,12 +422,13 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API ID', 'API 명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'apiName', align: 'left', width: '250', sortable: false },
|
{ name: 'apiName', align: 'left', width: '150', sortable: false },
|
||||||
|
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
|
||||||
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
@@ -463,7 +465,7 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
@@ -510,11 +512,21 @@
|
|||||||
$("input[name=searchStartDateTime]").inputmask("9999-99", { 'autoUnmask': true });
|
$("input[name=searchStartDateTime]").inputmask("9999-99", { 'autoUnmask': true });
|
||||||
$("input[name=searchEndDateTime]").inputmask("9999-99", { 'autoUnmask': true });
|
$("input[name=searchEndDateTime]").inputmask("9999-99", { 'autoUnmask': true });
|
||||||
|
|
||||||
// 기본값 설정 (최근 12개월)
|
// 기본값 설정: 올해 1월 ~ 지난달 (현재가 1월이면 작년 1월 ~ 작년 12월)
|
||||||
var today = new Date();
|
var today = new Date();
|
||||||
var endMonth = today.getFullYear() + '-' + (today.getMonth() + 1 < 10 ? '0' : '') + (today.getMonth() + 1);
|
var startDate, endDate;
|
||||||
var startDate = new Date(today.getFullYear(), today.getMonth() - 11, 1);
|
if (today.getMonth() === 0) { // 1월
|
||||||
var startMonth = startDate.getFullYear() + '-' + (startDate.getMonth() + 1 < 10 ? '0' : '') + (startDate.getMonth() + 1);
|
startDate = new Date(today.getFullYear() - 1, 0, 1);
|
||||||
|
endDate = new Date(today.getFullYear() - 1, 11, 1);
|
||||||
|
} else {
|
||||||
|
startDate = new Date(today.getFullYear(), 0, 1);
|
||||||
|
endDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
||||||
|
}
|
||||||
|
function toMonthStr(d) {
|
||||||
|
return d.getFullYear() + '-' + (d.getMonth() + 1 < 10 ? '0' : '') + (d.getMonth() + 1);
|
||||||
|
}
|
||||||
|
var startMonth = toMonthStr(startDate);
|
||||||
|
var endMonth = toMonthStr(endDate);
|
||||||
|
|
||||||
if (!$("input[name=searchStartDateTime]").val()) {
|
if (!$("input[name=searchStartDateTime]").val()) {
|
||||||
$("input[name=searchStartDateTime]").val(startMonth);
|
$("input[name=searchStartDateTime]").val(startMonth);
|
||||||
@@ -593,7 +605,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>API명</th>
|
<th>API ID</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||||
</td>
|
</td>
|
||||||
@@ -635,7 +647,7 @@
|
|||||||
<!-- 요약 그리드 -->
|
<!-- 요약 그리드 -->
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<div class="title" style="margin: 0;">API명별 요약 통계</div>
|
<div class="title" style="margin: 0;">API별 요약 통계</div>
|
||||||
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
||||||
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
|
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||||
title: {
|
title: {
|
||||||
text: '총 건수 분포',
|
text: '총 건수 분포',
|
||||||
left: 'center',
|
left: 'center',
|
||||||
@@ -99,7 +100,7 @@
|
|||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
]
|
]
|
||||||
@@ -397,12 +398,13 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API ID', 'API 명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'apiName', align: 'left', width: '250', sortable: false },
|
{ name: 'apiName', align: 'left', width: '150', sortable: false },
|
||||||
|
{ name: 'apiDesc', align: 'left', width: '250', sortable: false },
|
||||||
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
@@ -439,7 +441,7 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
@@ -563,7 +565,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>API명</th>
|
<th>API ID</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||||
</td>
|
</td>
|
||||||
@@ -604,7 +606,7 @@
|
|||||||
<!-- 요약 그리드 -->
|
<!-- 요약 그리드 -->
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<div class="title" style="margin: 0;">API명별 요약 통계</div>
|
<div class="title" style="margin: 0;">API별 요약 통계</div>
|
||||||
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
||||||
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -19,6 +19,14 @@
|
|||||||
#gbox_grid .ui-jqgrid-ftable tr.footrow td {
|
#gbox_grid .ui-jqgrid-ftable tr.footrow td {
|
||||||
height: 26px;
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
.cssbtn.small {
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 2px;
|
||||||
|
min-width: 24px;
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: #ebebec;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script language="javascript">
|
<script language="javascript">
|
||||||
var url = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.json"/>';
|
var url = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.json"/>';
|
||||||
@@ -65,13 +73,53 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 조회구분(searchType)에 따라 '구분' / '구분명' 컬럼의 헤더명·폭·표시여부를 조정
|
||||||
|
function applyGridColumnsByType(type) {
|
||||||
|
var $g = $("#grid");
|
||||||
|
if (type === 'API') {
|
||||||
|
$g.jqGrid('setLabel', 'orgName', 'API ID');
|
||||||
|
$g.jqGrid('setLabel', 'apiDesc', 'API명');
|
||||||
|
$g.jqGrid('setColProp', 'orgName', { widthOrg: 150, width: 150 });
|
||||||
|
$g.jqGrid('setColProp', 'apiDesc', { widthOrg: 250, width: 250 });
|
||||||
|
$g.jqGrid('showCol', 'apiDesc');
|
||||||
|
} else {
|
||||||
|
$g.jqGrid('setLabel', 'orgName', (type === 'DATE') ? '사용일' : '제휴사');
|
||||||
|
$g.jqGrid('setColProp', 'orgName', { widthOrg: 250, width: 250 });
|
||||||
|
$g.jqGrid('hideCol', 'apiDesc');
|
||||||
|
}
|
||||||
|
$g.jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
|
}
|
||||||
|
|
||||||
function search() {
|
function search() {
|
||||||
var postData = getPostData("cmd", "LIST");
|
var postData = getPostData("cmd", "LIST");
|
||||||
if (postData) {
|
if (postData) {
|
||||||
|
applyGridColumnsByType($('input[name="searchType"]:checked').val());
|
||||||
$("#grid").setGridParam({ url: url, postData: postData }).trigger("reloadGrid");
|
$("#grid").setGridParam({ url: url, postData: postData }).trigger("reloadGrid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 조회기간을 시작일 기준 이전/다음 달의 1일~말일로 변경 후 조회 (direction: -1 이전달, 1 다음달)
|
||||||
|
function shiftMonthRange(direction) {
|
||||||
|
var v = $("input[name=searchStartDate]").val().replace(/-/g, "");
|
||||||
|
if (!v || v.length < 6) return;
|
||||||
|
|
||||||
|
var year = parseInt(v.substring(0, 4));
|
||||||
|
var month = parseInt(v.substring(4, 6)) - 1; // 0-based
|
||||||
|
|
||||||
|
var first = new Date(year, month + direction, 1);
|
||||||
|
var last = new Date(year, month + direction + 1, 0); // 해당 달의 말일
|
||||||
|
|
||||||
|
function pad(n) { return String(n).padStart(2, '0'); }
|
||||||
|
function fmt(d) {
|
||||||
|
return d.getFullYear() + '' + pad(d.getMonth() + 1) + pad(d.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
$("input[name=searchStartDate]").val(fmt(first));
|
||||||
|
$("input[name=searchEndDate]").val(fmt(last));
|
||||||
|
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
|
||||||
function exportToExcel() {
|
function exportToExcel() {
|
||||||
|
|
||||||
var postData = getPostData("cmd", "EXCEL_EXPORT");
|
var postData = getPostData("cmd", "EXCEL_EXPORT");
|
||||||
@@ -186,12 +234,13 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'구분',
|
'제휴사', 'API명',
|
||||||
'총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류',
|
'총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류',
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'orgName', align: 'left', width: '250', sortable: false },
|
{ name: 'orgName', align: 'left', width: '250', sortable: false },
|
||||||
|
{ name: 'apiDesc', align: 'left', width: '250', sortable: false, hidden: true },
|
||||||
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'successRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'successRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
@@ -225,9 +274,22 @@
|
|||||||
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||||
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
|
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
|
||||||
|
|
||||||
|
// 기본값 설정: 이번달 1일 ~ 어제 (오늘이 1일이면 지난달 1일 ~ 지난달 말일)
|
||||||
var today = getToday();
|
var today = getToday();
|
||||||
var startDate = today.substring(0,6)+"01";
|
var now = new Date();
|
||||||
var endDate = today;
|
function toYmd(d) {
|
||||||
|
var m = d.getMonth() + 1;
|
||||||
|
var day = d.getDate();
|
||||||
|
return d.getFullYear() + '' + (m < 10 ? '0' : '') + m + (day < 10 ? '0' : '') + day;
|
||||||
|
}
|
||||||
|
var startDate, endDate;
|
||||||
|
if (now.getDate() === 1) {
|
||||||
|
startDate = toYmd(new Date(now.getFullYear(), now.getMonth() - 1, 1));
|
||||||
|
endDate = toYmd(new Date(now.getFullYear(), now.getMonth(), 0));
|
||||||
|
} else {
|
||||||
|
startDate = today.substring(0,6) + "01";
|
||||||
|
endDate = toYmd(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!$("input[name=searchStartDate]").val()) {
|
if (!$("input[name=searchStartDate]").val()) {
|
||||||
@@ -246,6 +308,14 @@
|
|||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btnPrevMonth").click(function() {
|
||||||
|
shiftMonthRange(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btnNextMonth").click(function() {
|
||||||
|
shiftMonthRange(1);
|
||||||
|
});
|
||||||
|
|
||||||
$("input[name=searchType]").click(function() {
|
$("input[name=searchType]").click(function() {
|
||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
@@ -288,6 +358,8 @@
|
|||||||
<input type="text" name="searchStartDate" id="searchStartDate" value="${param.searchStartDate}" style="width:100px;">
|
<input type="text" name="searchStartDate" id="searchStartDate" value="${param.searchStartDate}" style="width:100px;">
|
||||||
~
|
~
|
||||||
<input type="text" name="searchEndDate" id="searchEndDate" value="${param.searchEndDate}" style="width:100px;">
|
<input type="text" name="searchEndDate" id="searchEndDate" value="${param.searchEndDate}" style="width:100px;">
|
||||||
|
<button type="button" id="btnPrevMonth" class="cssbtn small"><</button>
|
||||||
|
<button type="button" id="btnNextMonth" class="cssbtn small">></button>
|
||||||
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
|
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -302,7 +374,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchOrgName" id="searchOrgName" value="${param.searchOrgName}">
|
<input type="text" name="searchOrgName" id="searchOrgName" value="${param.searchOrgName}">
|
||||||
</td>
|
</td>
|
||||||
<th style="width:120px;">API명</th>
|
<th style="width:120px;">API ID</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchApiName" id="searchApiName" value="${param.searchApiName}">
|
<input type="text" name="searchApiName" id="searchApiName" value="${param.searchApiName}">
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -38,6 +38,13 @@
|
|||||||
success: function (json) {
|
success: function (json) {
|
||||||
new makeOptions("BIZCODE", "BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering();
|
new makeOptions("BIZCODE", "BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering();
|
||||||
|
|
||||||
|
var apiGroupSelect = $('select[name=searchApiGroupId]').empty();
|
||||||
|
apiGroupSelect.append($('<option>').val('').text('전체'));
|
||||||
|
apiGroupSelect.append($('<option>').val('UNREGISTERED').text('미등록'));
|
||||||
|
(json.apiGroupList || []).forEach(function (group) {
|
||||||
|
apiGroupSelect.append($('<option>').val(group.id).text(group.groupName));
|
||||||
|
});
|
||||||
|
|
||||||
setSearchable(selectName); // 콤보에 searchable 설정
|
setSearchable(selectName); // 콤보에 searchable 설정
|
||||||
putSelectFromParam();
|
putSelectFromParam();
|
||||||
|
|
||||||
@@ -86,6 +93,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function specStatusFormatter(cellvalue, options, rowObject) {
|
||||||
|
switch (cellvalue) {
|
||||||
|
case 'PUBLISHED':
|
||||||
|
var groupNames = rowObject.publishedGroupNames || [];
|
||||||
|
var tooltip = groupNames.length ? groupNames.join('\n') : '게시된 그룹 없음';
|
||||||
|
return $('<span>').text('게시(' + groupNames.length + ')').attr('title', tooltip).css('cursor', 'help').prop('outerHTML');
|
||||||
|
case 'REGISTERED': return '등록';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function list() {
|
function list() {
|
||||||
detail()
|
detail()
|
||||||
var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로
|
var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로
|
||||||
@@ -101,6 +119,7 @@
|
|||||||
'요청',
|
'요청',
|
||||||
'응답',
|
'응답',
|
||||||
'상태',
|
'상태',
|
||||||
|
'스펙 상태',
|
||||||
'작성자',
|
'작성자',
|
||||||
'가상응답여부',
|
'가상응답여부',
|
||||||
'변경일시(*)',
|
'변경일시(*)',
|
||||||
@@ -113,6 +132,7 @@
|
|||||||
{name: 'fromAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
|
{name: 'fromAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
|
||||||
{name: 'toAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
|
{name: 'toAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
|
||||||
{name: 'statusCode', align: 'center', width: '30', formatter: apiStatusFormatter, sortable: false},
|
{name: 'statusCode', align: 'center', width: '30', formatter: apiStatusFormatter, sortable: false},
|
||||||
|
{name: 'specStatus', align: 'center', width: '40', formatter: specStatusFormatter, sortable: false, title: false},
|
||||||
{name: 'author', align: 'center', width: '40', sortable: false},
|
{name: 'author', align: 'center', width: '40', sortable: false},
|
||||||
{name : 'simYn', align : 'center' , width:'40', hidden: true },
|
{name : 'simYn', align : 'center' , width:'40', hidden: true },
|
||||||
{name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true},
|
{name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true},
|
||||||
@@ -421,6 +441,14 @@
|
|||||||
<td>
|
<td>
|
||||||
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"></select>
|
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"></select>
|
||||||
</td>
|
</td>
|
||||||
|
<th style="width:180px;">당타발 구분</th>
|
||||||
|
<td>
|
||||||
|
<select name="searchInOutType" style="width: 100%">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="1">당발</option>
|
||||||
|
<option value="2">타발</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
<th style="width:180px;">상태</th>
|
<th style="width:180px;">상태</th>
|
||||||
<td>
|
<td>
|
||||||
<select name="searchStatusCode" style="width: 100%">
|
<select name="searchStatusCode" style="width: 100%">
|
||||||
@@ -431,6 +459,24 @@
|
|||||||
<option value="C">점검</option>
|
<option value="C">점검</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:180px;">API 스펙 상태</th>
|
||||||
|
<td>
|
||||||
|
<select name="searchSpecStatus" style="width: 100%">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="UNREGISTERED">미등록</option>
|
||||||
|
<option value="REGISTERED">등록</option>
|
||||||
|
<option value="PUBLISHED">게시</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<th style="width:180px;">API 그룹</th>
|
||||||
|
<td>
|
||||||
|
<select name="searchApiGroupId" style="width: 100%">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="UNREGISTERED">미등록</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
<th></th><td></td>
|
<th></th><td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -281,6 +281,9 @@ $(document).ready(function() {
|
|||||||
<%-- <img id="btn_excel" src="<c:url value="/img/btn_excel.png"/>" level="W" status="DETAIL"/> --%>
|
<%-- <img id="btn_excel" src="<c:url value="/img/btn_excel.png"/>" level="W" status="DETAIL"/> --%>
|
||||||
</div>
|
</div>
|
||||||
<div class="title"><%= localeMessage.getString("tranStat.title") %><span class="tooltip"><%= localeMessage.getString("tranStat.tooltip") %></span></div>
|
<div class="title"><%= localeMessage.getString("tranStat.title") %><span class="tooltip"><%= localeMessage.getString("tranStat.tooltip") %></span></div>
|
||||||
|
<div style="padding-bottom: 10px; color: #555">
|
||||||
|
※ 100 : 송신>APIM 200 : APIM>수신 300 : 수신>APIM 400 : APIM>송신
|
||||||
|
</div>
|
||||||
<ajax>
|
<ajax>
|
||||||
<table id="grid" ></table>
|
<table id="grid" ></table>
|
||||||
</ajax>
|
</ajax>
|
||||||
|
|||||||
@@ -437,6 +437,9 @@ $( document ).ready(function() {
|
|||||||
<%-- <img src="<c:url value="/img/btn_search.png"/>" alt="" id="btn_search" level="R" /> --%>
|
<%-- <img src="<c:url value="/img/btn_search.png"/>" alt="" id="btn_search" level="R" /> --%>
|
||||||
</div>
|
</div>
|
||||||
<div class="title"><%= localeMessage.getString("tranStatDetail.title") %><span class="tooltip"><%= localeMessage.getString("tranStat.tooltip") %></span></div>
|
<div class="title"><%= localeMessage.getString("tranStatDetail.title") %><span class="tooltip"><%= localeMessage.getString("tranStat.tooltip") %></span></div>
|
||||||
|
<div style="padding-bottom: 10px; color: #555">
|
||||||
|
※ 100 : 송신>APIM 200 : APIM>수신 300 : 수신>APIM 400 : APIM>송신
|
||||||
|
</div>
|
||||||
<table class="search_condition" cellspacing=0;>
|
<table class="search_condition" cellspacing=0;>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -407,7 +407,7 @@
|
|||||||
$("#grid").tuiTableRowSpan("7");
|
$("#grid").tuiTableRowSpan("7");
|
||||||
|
|
||||||
//1000건이상 alert
|
//1000건이상 alert
|
||||||
if (d.records == 1000) {
|
if (d.records >= 1000) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
alert("<%=localeMessage.getString("tracking.alert5")%>"); // 조회 결과 1,000건 초과
|
alert("<%=localeMessage.getString("tracking.alert5")%>"); // 조회 결과 1,000건 초과
|
||||||
}, 0); // Alert는 비동기적으로 처리
|
}, 0); // Alert는 비동기적으로 처리
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table_row td { padding: 0 5px; }
|
||||||
|
.table_row input { box-sizing: border-box; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script language="javascript" >
|
<script language="javascript" >
|
||||||
// var eaiBzwkDstcd = window.dialogArguments["eaiBzwkDstcd"];
|
// var eaiBzwkDstcd = window.dialogArguments["eaiBzwkDstcd"];
|
||||||
@@ -253,7 +256,7 @@
|
|||||||
var name = $(this).attr("name");
|
var name = $(this).attr("name");
|
||||||
if ("${rmsMenuAuth}" =="W"){ //2020.07.08 admin 만 버튼보이게 수정
|
if ("${rmsMenuAuth}" =="W"){ //2020.07.08 admin 만 버튼보이게 수정
|
||||||
$("#"+name).show();
|
$("#"+name).show();
|
||||||
$("input[name="+name+"]").css("width","185px");
|
$("input[name="+name+"]").css("width","calc(100% - 60px)");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -261,7 +264,7 @@
|
|||||||
var name = $(this).attr("name");
|
var name = $(this).attr("name");
|
||||||
if($("input[name="+name+"]").val().trim() != ""){
|
if($("input[name="+name+"]").val().trim() != ""){
|
||||||
$("#"+name).show();
|
$("#"+name).show();
|
||||||
$("input[name="+name+"]").css("width","185px");
|
$("input[name="+name+"]").css("width","calc(100% - 60px)");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -977,15 +980,23 @@ html,hbody {
|
|||||||
<input type=hidden name=realHeader>
|
<input type=hidden name=realHeader>
|
||||||
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.common")%></div>
|
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.common")%></div>
|
||||||
<table class="table_row" cellspacing="0">
|
<table class="table_row" cellspacing="0">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
</colgroup>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%=localeMessage.getString("trackingDetail.eaiSvcSerno")%></th>
|
<th><%=localeMessage.getString("trackingDetail.eaiSvcSerno")%></th>
|
||||||
<td colspan="3"><input type="text" name="eaiSvcSerno" readonly="readonly"/> </td>
|
<td><input type="text" name="eaiSvcSerno" readonly="readonly"/> </td>
|
||||||
|
<th><%=localeMessage.getString("trackingDetail.rspnsErrcdName")%></th>
|
||||||
|
<td><input type="text" name="rspnsErrcdName" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.eaiBzwkDstcd")%></th>
|
<th><%=localeMessage.getString("trackingDetail.eaiBzwkDstcd")%></th>
|
||||||
<td style="width:30%;"><input type="text" name="eaiBzwkDstcd" readonly="readonly"/> </td>
|
<td><input type="text" name="eaiBzwkDstcd" readonly="readonly"/> </td>
|
||||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.msgDpstYMS")%></th>
|
<th><%=localeMessage.getString("trackingDetail.msgDpstYMS")%></th>
|
||||||
<td style="width:30%;"><input type="text" name="msgDpstYMS" readonly="readonly"/> </td>
|
<td><input type="text" name="msgDpstYMS" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th><td><input type="text" name="eaiSvcName" readonly="readonly" />
|
<th><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th><td><input type="text" name="eaiSvcName" readonly="readonly" />
|
||||||
@@ -1019,11 +1030,17 @@ html,hbody {
|
|||||||
</table>
|
</table>
|
||||||
<div class="table_row_title">API 정보</div>
|
<div class="table_row_title">API 정보</div>
|
||||||
<table class="table_row" cellspacing="0">
|
<table class="table_row" cellspacing="0">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
</colgroup>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:20%;">APP(Client) ID</th>
|
<th>APP(Client) ID</th>
|
||||||
<td style="width:30%;"><input type="text" name="clientId" readonly="readonly"/> </td>
|
<td><input type="text" name="clientId" readonly="readonly"/> </td>
|
||||||
<th style="width:20%;">APP 명</th>
|
<th>APP 명</th>
|
||||||
<td style="width:30%;"><input type="text" name="clientName" readonly="readonly"/> </td>
|
<td><input type="text" name="clientName" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>법인 ID</th><td ><input type="text" name="orgId" readonly="readonly"/> </td>
|
<th>법인 ID</th><td ><input type="text" name="orgId" readonly="readonly"/> </td>
|
||||||
@@ -1145,20 +1162,27 @@ html,hbody {
|
|||||||
</div><!-- end.container-1 -->
|
</div><!-- end.container-1 -->
|
||||||
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.inbound")%></div>
|
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.inbound")%></div>
|
||||||
<table class="table_row" cellspacing="0" >
|
<table class="table_row" cellspacing="0" >
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
</colgroup>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.svcMotivUseDstcd")%></th>
|
<%-- <th style="width:20%;"><%=localeMessage.getString("trackingDetail.svcMotivUseDstcd")%></th>
|
||||||
<td style="width:30%;"><input type="text" name="svcMotivUseDstcd" readonly="readonly"/> </td>
|
<td style="width:30%;"><input type="text" name="svcMotivUseDstcd" readonly="readonly"/> </td> --%>
|
||||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.gstatSysAdptrBzwkGroupName")%></th>
|
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.gstatSysAdptrBzwkGroupName")%></th>
|
||||||
<td style="width:30%;"><input type="text" name="gstatSysAdptrBzwkGroupName" readonly="readonly"/>
|
<td style="width:30%;"><input type="text" name="gstatSysAdptrBzwkGroupName" readonly="readonly" style="width: calc(100% - 60px);"/>
|
||||||
<%-- <img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="gstatSysAdptrBzwkGroupName" class="adapter_btn" level="W"/> --%>
|
<%-- <img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="gstatSysAdptrBzwkGroupName" class="adapter_btn" level="W"/> --%>
|
||||||
<button type="button" class="cssbtn smallBtn2" id="gstatSysAdptrBzwkGroupName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
|
<button type="button" class="cssbtn smallBtn2" id="gstatSysAdptrBzwkGroupName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th><%=localeMessage.getString("trackingDetail.flowCtrlRoutName")%></th><td><input type="text" name="flowCtrlRoutName" readonly="readonly"/> </td>
|
|
||||||
<th><%=localeMessage.getString("trackingDetail.stndMsgUseYn")%></th><td><input type="text" name="stndMsgUseYn" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.stndMsgUseYn")%></th><td><input type="text" name="stndMsgUseYn" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<%-- <tr>
|
||||||
|
<th><%=localeMessage.getString("trackingDetail.flowCtrlRoutName")%></th><td><input type="text" name="flowCtrlRoutName" readonly="readonly"/> </td>
|
||||||
|
|
||||||
|
</tr> --%>
|
||||||
|
<%-- <tr>
|
||||||
<th><%=localeMessage.getString("trackingDetail.svcPrcssDsticName")%></th><td><input type="text" name="svcPrcssDsticName" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.svcPrcssDsticName")%></th><td><input type="text" name="svcPrcssDsticName" readonly="readonly"/> </td>
|
||||||
<th><%=localeMessage.getString("trackingDetail.svcBfClmnLogYn")%></th><td><input type="text" name="svcBfClmnLogYn" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.svcBfClmnLogYn")%></th><td><input type="text" name="svcBfClmnLogYn" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1169,39 +1193,52 @@ html,hbody {
|
|||||||
<tr>
|
<tr>
|
||||||
<th><%=localeMessage.getString("trackingDetail.gstatSvcDsticName")%></th><td><input type="text" name="gstatSvcDsticName" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.gstatSvcDsticName")%></th><td><input type="text" name="gstatSvcDsticName" readonly="readonly"/> </td>
|
||||||
<th><%=localeMessage.getString("trackingDetail.prsntMsgIdName")%></th><td><input type="text" name="prsntMsgIdName" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.prsntMsgIdName")%></th><td><input type="text" name="prsntMsgIdName" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr> --%>
|
||||||
</table>
|
</table>
|
||||||
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.outbound")%></div>
|
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.outbound")%></div>
|
||||||
<table class="table_row" cellspacing="0" >
|
<table class="table_row" cellspacing="0" >
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
</colgroup>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvIntfacDsticName")%></th>
|
<%-- <th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvIntfacDsticName")%></th>--%>
|
||||||
<td style="width:30%;"><input type="text" name="psvIntfacDsticName" readonly="readonly"/> </td>
|
<%-- <td style="width:30%;"><input type="text" name="psvIntfacDsticName" readonly="readonly"/> </td> --%>
|
||||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvSysAdptrBzwkGroupName")%></th>
|
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvSysAdptrBzwkGroupName")%></th>
|
||||||
<td style="width:30%;"><input type="text" name="psvSysAdptrBzwkGroupName" readonly="readonly"/>
|
<td style="width:30%;"><input type="text" name="psvSysAdptrBzwkGroupName" readonly="readonly" style="width: calc(100% - 60px);"/>
|
||||||
<%-- <img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="psvSysAdptrBzwkGroupName" class="adapter_btn" level="W"/> --%>
|
<%-- <img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="psvSysAdptrBzwkGroupName" class="adapter_btn" level="W"/> --%>
|
||||||
<button type="button" class="cssbtn smallBtn2" id="psvSysAdptrBzwkGroupName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
|
<button type="button" class="cssbtn smallBtn2" id="psvSysAdptrBzwkGroupName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
|
||||||
</td>
|
</td>
|
||||||
|
<th><%=localeMessage.getString("trackingDetail.psvBzwkSysName")%></th><td><input type="text" name="psvBzwkSysName" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%=localeMessage.getString("trackingDetail.outbndRoutName")%></th><td><input type="text" name="outbndRoutName" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.outbndRoutName")%></th><td><input type="text" name="outbndRoutName" readonly="readonly"/> </td>
|
||||||
<th><%=localeMessage.getString("trackingDetail.toutVal")%></th><td><input type="text" name="toutVal" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.toutVal")%></th><td><input type="text" name="toutVal" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<%-- <tr>
|
||||||
<th><%=localeMessage.getString("trackingDetail.psvSysSvcDsticName")%></th><td><input type="text" name="psvSysSvcDsticName" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.psvSysSvcDsticName")%></th><td><input type="text" name="psvSysSvcDsticName" readonly="readonly"/> </td>
|
||||||
<th><%=localeMessage.getString("trackingDetail.psvBzwkSysName")%></th><td><input type="text" name="psvBzwkSysName" readonly="readonly"/> </td>
|
|
||||||
</tr>
|
</tr>--%>
|
||||||
<tr>
|
<%-- <tr>
|
||||||
<th><%=localeMessage.getString("trackingDetail.rspnsErrFldName")%></th><td><input type="text" name="rspnsErrFldName" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.rspnsErrFldName")%></th><td><input type="text" name="rspnsErrFldName" readonly="readonly"/> </td>
|
||||||
<th><%=localeMessage.getString("trackingDetail.psvSysIdName")%></th><td><input type="text" name="psvSysIdName" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.psvSysIdName")%></th><td><input type="text" name="psvSysIdName" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr> --%>
|
||||||
</table>
|
</table>
|
||||||
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.MAPPING")%></div>
|
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.MAPPING")%></div>
|
||||||
<table class="table_row" cellspacing="0" >
|
<table class="table_row" cellspacing="0" >
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
<col style="width:180px"/>
|
||||||
|
<col style="width:300px" />
|
||||||
|
</colgroup>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.chngYn")%></th>
|
<th><%=localeMessage.getString("trackingDetail.chngYn")%></th>
|
||||||
<td style="width:30%;"><input type="text" name="chngYn" readonly="readonly"/> </td>
|
<td><input type="text" name="chngYn" readonly="readonly"/> </td>
|
||||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.chngMsgIdName")%></th>
|
<th><%=localeMessage.getString("trackingDetail.chngMsgIdName")%></th>
|
||||||
<td style="width:30%;"><input type="text" name="chngMsgIdName" readonly="readonly"/>
|
<td><input type="text" name="chngMsgIdName" readonly="readonly" style="width: calc(100% - 60px);"/>
|
||||||
<!-- img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="chngMsgIdName" class="layout_btn" /-->
|
<!-- img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="chngMsgIdName" class="layout_btn" /-->
|
||||||
<button type="button" class="cssbtn smallBtn2" id="chngMsgIdName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
|
<button type="button" class="cssbtn smallBtn2" id="chngMsgIdName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
|
||||||
</td>
|
</td>
|
||||||
@@ -1210,7 +1247,7 @@ html,hbody {
|
|||||||
<th><%=localeMessage.getString("trackingDetail.bascRspnsChngYn")%></th><td><input type="text" name="bascRspnsChngYn" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.bascRspnsChngYn")%></th><td><input type="text" name="bascRspnsChngYn" readonly="readonly"/> </td>
|
||||||
<th><%=localeMessage.getString("trackingDetail.bascRspnsChngMsgIdName")%></th>
|
<th><%=localeMessage.getString("trackingDetail.bascRspnsChngMsgIdName")%></th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="bascRspnsChngMsgIdName" readonly="readonly"/>
|
<input type="text" name="bascRspnsChngMsgIdName" readonly="readonly" style="width: calc(100% - 60px);"/>
|
||||||
<!-- img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="bascRspnsChngMsgIdName" class="layout_btn" /-->
|
<!-- img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="bascRspnsChngMsgIdName" class="layout_btn" /-->
|
||||||
<button type="button" class="cssbtn smallBtn2" id="bascRspnsChngMsgIdName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
|
<button type="button" class="cssbtn smallBtn2" id="bascRspnsChngMsgIdName" style="vertical-align:middle; font-weight:bold;" level="W"><i class="material-icons">details</i><%= localeMessage.getString("button.detail") %></button>
|
||||||
</td>
|
</td>
|
||||||
@@ -1220,10 +1257,10 @@ html,hbody {
|
|||||||
<th><%=localeMessage.getString("trackingDetail.errRspnsChngYn")%></th><td><input type="text" name="errRspnsChngYn" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.errRspnsChngYn")%></th><td><input type="text" name="errRspnsChngYn" readonly="readonly"/> </td>
|
||||||
<th><%=localeMessage.getString("trackingDetail.errRspnsChngMsgIdName")%></th>
|
<th><%=localeMessage.getString("trackingDetail.errRspnsChngMsgIdName")%></th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="errRspnsChngMsgIdName" readonly="readonly"/>
|
<input type="text" name="errRspnsChngMsgIdName" readonly="readonly" style="width: calc(100% - 60px);"/>
|
||||||
<img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="errRspnsChngMsgIdName" class="layout_btn" /> </td>
|
<img src="<c:url value="/images/bt/pop_detail.gif"/>" alt="" id="errRspnsChngMsgIdName" class="layout_btn" /> </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<%-- <tr>
|
||||||
<th><%=localeMessage.getString("trackingDetail.inptMsgIDName")%></th>
|
<th><%=localeMessage.getString("trackingDetail.inptMsgIDName")%></th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="inptMsgIDName" readonly="readonly" />
|
<input type="text" name="inptMsgIDName" readonly="readonly" />
|
||||||
@@ -1233,9 +1270,9 @@ html,hbody {
|
|||||||
<tr>
|
<tr>
|
||||||
<th><%=localeMessage.getString("trackingDetail.bascRspnsMsgCmprCtnt")%></th><td><input type="text" name="bascRspnsMsgCmprCtnt" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.bascRspnsMsgCmprCtnt")%></th><td><input type="text" name="bascRspnsMsgCmprCtnt" readonly="readonly"/> </td>
|
||||||
<th><%=localeMessage.getString("trackingDetail.flovrYn")%></th><td><input type="text" name="flovrYn" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.flovrYn")%></th><td><input type="text" name="flovrYn" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr> --%>
|
||||||
</table>
|
</table>
|
||||||
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.etc")%></div>
|
<%-- <div class="table_row_title"><%=localeMessage.getString("trackingDetail.etc")%></div>
|
||||||
<table class="table_row" cellspacing="0" >
|
<table class="table_row" cellspacing="0" >
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.rspnsErrcdName")%></th>
|
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.rspnsErrcdName")%></th>
|
||||||
@@ -1267,7 +1304,7 @@ html,hbody {
|
|||||||
<th><%=localeMessage.getString("trackingDetail.trackAsisKey3Ctnt")%></th><td><input type="text" name="trackAsisKey3Ctnt" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.trackAsisKey3Ctnt")%></th><td><input type="text" name="trackAsisKey3Ctnt" readonly="readonly"/> </td>
|
||||||
<th><%=localeMessage.getString("trackingDetail.trackAsisKey4Ctnt")%></th><td><input type="text" name="trackAsisKey4Ctnt" readonly="readonly"/> </td>
|
<th><%=localeMessage.getString("trackingDetail.trackAsisKey4Ctnt")%></th><td><input type="text" name="trackAsisKey4Ctnt" readonly="readonly"/> </td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table> --%>
|
||||||
</form>
|
</form>
|
||||||
</div><!-- end.popup_box -->
|
</div><!-- end.popup_box -->
|
||||||
<div id="bizLogObjectDialog" style="white-space: pre-wrap; overflow-y:auto" title="Biz Log Object Detail">
|
<div id="bizLogObjectDialog" style="white-space: pre-wrap; overflow-y:auto" title="Biz Log Object Detail">
|
||||||
|
|||||||
+13
-3
@@ -113,9 +113,9 @@ dependencies {
|
|||||||
// https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
|
// https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
|
||||||
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
|
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
|
||||||
|
|
||||||
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1'
|
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.2.2'
|
||||||
//implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
//implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.2'
|
||||||
|
|
||||||
// apistatus-draft.yml (자동 탐지 초안 양식) 로딩용.
|
// apistatus-draft.yml (자동 탐지 초안 양식) 로딩용.
|
||||||
// admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없어 직접 파싱한다.
|
// admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없어 직접 파싱한다.
|
||||||
@@ -140,13 +140,18 @@ dependencies {
|
|||||||
|
|
||||||
implementation "org.apache.ibatis:ibatis-sqlmap:2.3.4.726"
|
implementation "org.apache.ibatis:ibatis-sqlmap:2.3.4.726"
|
||||||
implementation "org.jdom:jdom:1.1"
|
implementation "org.jdom:jdom:1.1"
|
||||||
implementation "com.google.code.gson:gson:2.3.1"
|
implementation "com.google.code.gson:gson:2.8.9"
|
||||||
implementation "javax.annotation:javax.annotation-api:1.2"
|
implementation "javax.annotation:javax.annotation-api:1.2"
|
||||||
implementation "net.sf.json-lib:json-lib-ext-spring:1.0.2"
|
implementation "net.sf.json-lib:json-lib-ext-spring:1.0.2"
|
||||||
implementation "taglibs:standard:1.1.2"
|
implementation "taglibs:standard:1.1.2"
|
||||||
implementation "javax.servlet:jstl:1.1.2"
|
implementation "javax.servlet:jstl:1.1.2"
|
||||||
implementation group: 'com.jcraft', name: 'jsch', version: '0.1.51'
|
implementation group: 'com.jcraft', name: 'jsch', version: '0.1.51'
|
||||||
|
|
||||||
|
// springapp-servlet.xml 의 CommonsMultipartResolver 가 사용.
|
||||||
|
// 기존엔 elink-online-common(api 'commons-fileupload:1.5')에서 전이적으로 받아왔으나,
|
||||||
|
// eapim-online 쪽에서 해당 의존성을 제거할 예정이라 직접 선언으로 전환.
|
||||||
|
implementation "commons-fileupload:commons-fileupload:1.6.0"
|
||||||
|
|
||||||
implementation "org.quartz-scheduler:quartz:${quartzVersion}"
|
implementation "org.quartz-scheduler:quartz:${quartzVersion}"
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/org.apache.poi/poi
|
// https://mvnrepository.com/artifact/org.apache.poi/poi
|
||||||
@@ -234,6 +239,11 @@ configurations.all {
|
|||||||
// 일부 transitive 가 끌어오는 xml-apis:1.0.b2 (2002, DOM L2) 는 이 클래스를 포함하지 않아
|
// 일부 transitive 가 끌어오는 xml-apis:1.0.b2 (2002, DOM L2) 는 이 클래스를 포함하지 않아
|
||||||
// xercesImpl 가 NoClassDefFoundError 를 일으킴 → 1.4.01 로 강제 통일.
|
// xercesImpl 가 NoClassDefFoundError 를 일으킴 → 1.4.01 로 강제 통일.
|
||||||
force 'xml-apis:xml-apis:1.4.01'
|
force 'xml-apis:xml-apis:1.4.01'
|
||||||
|
|
||||||
|
// logback 1.2.10 은 SLF4J 1.7 바인딩(StaticLoggerBinder)만 제공한다.
|
||||||
|
// transitive (Java-WebSocket:1.5.7 등) 가 slf4j-api 2.x 를 끌어올리면
|
||||||
|
// SLF4J 가 provider 를 못 찾아 NOP 로거로 폴백 → 콘솔/파일 로그가 전부 사라진다.
|
||||||
|
force 'org.slf4j:slf4j-api:1.7.36'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
POST http://localhost:7090/monitoring/_onl/admin/authserver/clientMan.json?serviceType=APIGW
|
POST http://localhost:7090/monitoring/_onl/admin/authserver/clientMan.json?serviceType=APIGW
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
User-Agent: insomnia/10.1.1
|
User-Agent: insomnia/10.1.1
|
||||||
X-API-KEY: EAPIM_KEY
|
X-Internal-Token: {{internalApiToken}}
|
||||||
action: insert
|
action: insert
|
||||||
target_host: stg
|
target_host: stg
|
||||||
|
|
||||||
|
|||||||
@@ -1,470 +1,368 @@
|
|||||||
# EAPIM Admin
|
# eapim-admin
|
||||||
|
|
||||||
광주은행 엔터프라이즈 API 관리 시스템 - 관리 콘솔
|
제주은행(DJB) eLink EMS 관리 콘솔
|
||||||
|
|
||||||
## 프로젝트 개요
|
## 개요
|
||||||
|
|
||||||
**EAPIM Admin**은 광주은행의 eLink EMS (eLink Management System) 관리 콘솔로, API 게이트웨이 구성, 모니터링, 통계, 트랜잭션 로그 조회 기능을 제공하는 웹 기반 플랫폼입니다.
|
**eapim-admin** 은 eLink EMS(eLink Management System)의 웹 기반 관리 콘솔입니다.
|
||||||
|
API 게이트웨이 구성, 실시간 모니터링/대시보드, 통계, 트랜잭션 로그 조회, 인터페이스·레이아웃·어댑터·라우팅 관리 기능을 제공합니다.
|
||||||
|
|
||||||
|
eLink EMS 는 세 가지 애플리케이션으로 구성됩니다.
|
||||||
|
|
||||||
|
| 애플리케이션 | 설명 |
|
||||||
|
|---|---|
|
||||||
|
| **eapim-admin** | 관리 콘솔 (이 저장소) |
|
||||||
|
| eapim-online | 실시간 트랜잭션 처리 API 게이트웨이 |
|
||||||
|
| eapim-portal | API 소비자용 개발자 포털 (Spring Boot) |
|
||||||
|
|
||||||
|
> 패키지·클래스에 남아 있는 `kjb` / `KJB` 명칭은 초기 광주은행 커스터마이징에서 유래한 것으로,
|
||||||
|
> 현재 배포 대상은 제주은행(DJB)입니다. 제주은행 전용 코드는 `com.eactive.eai.rms.ext.djb` 하위에 있습니다.
|
||||||
|
|
||||||
## 기술 스택
|
## 기술 스택
|
||||||
|
|
||||||
- Spring Framework 5.3.27
|
| 구분 | 버전 / 내용 |
|
||||||
- Java 17
|
|---|---|
|
||||||
- Gradle 8.7
|
| Java | **8** (Gradle toolchain 으로 강제, `JavaLanguageVersion.of(8)`) |
|
||||||
- Oracle 19c
|
| 빌드 | Gradle **8.7** (wrapper), `war` 패키징 |
|
||||||
- Spring Data JPA 2.5.2
|
| Spring Framework | 5.3.27 (MVC / ORM / JDBC) |
|
||||||
- Hibernate 5.4.33/5.6.15
|
| Spring Data JPA | 2.5.2 |
|
||||||
- QueryDSL 5.0.0
|
| Hibernate | 5.6.15.Final |
|
||||||
- iBATIS 2.3.4 (레거시 SQL 매핑)
|
| QueryDSL | 5.0.0 (빌드 시 Q-class 생성) |
|
||||||
- Quartz 2.2.1
|
| iBATIS | 2.3.4 (레거시 SQL 매핑) |
|
||||||
- Logback 1.2.10
|
| Quartz | 2.2.1 (스케줄링) |
|
||||||
|
| 로깅 | SLF4J + Logback 1.2.10 (모든 `log4j` 의존성 제외) |
|
||||||
|
| 기타 | Apache POI 3.17(Excel), Jackson 2.13.1, AWS SDK 2.x(S3), EhCache, Lombok, MapStruct |
|
||||||
|
|
||||||
## 초기 세팅 가이드
|
애플리케이션 프레임워크는 Spring Boot 가 아닌 전통적 Spring 5.3 WAR 이며 Tomcat / WebLogic 등에 배포됩니다.
|
||||||
|
(Spring Boot 는 테스트 유틸리티 `spring-boot-starter-test:2.6.15` 에만 사용)
|
||||||
|
|
||||||
### 사전 요구사항
|
## 사전 요구사항
|
||||||
|
|
||||||
- JDK 17 이상
|
- **JDK 8** - `.envrc`(direnv) 또는 셸 rc 에서 `JAVA_HOME` 을 JDK 8 위치로 export. Gradle 8 toolchain auto-detect 가 `JAVA_HOME` 도 후보로 인식합니다. `gradle.properties` 에 JDK 경로를 넣지 않습니다.
|
||||||
- Gradle 8.7
|
|
||||||
- Git
|
- Git
|
||||||
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
|
- Oracle 접근 (개발/테스트 환경에 따라). 테스트는 H2 인메모리 DB 사용.
|
||||||
|
- **형제 디렉토리 체크아웃 필요** - 멀티 모듈 프로젝트로 아래 저장소가 같은 부모 디렉토리에 있어야 합니다.
|
||||||
|
```
|
||||||
|
<workspace>/
|
||||||
|
├── eapim-admin/ # 이 저장소
|
||||||
|
├── eapim-online/ # elink-online-* 모듈
|
||||||
|
└── elink-portal-common/ # 포털 공통 JPA 엔티티
|
||||||
|
```
|
||||||
|
|
||||||
### 환경별 설정 파일
|
## 멀티 모듈 구조
|
||||||
프로젝트 클론 후 필요한 환경 설정을 수정하세요:
|
|
||||||
|
|
||||||
- `WebContent/WEB-INF/properties/env.D.properties` - 개발 환경
|
`settings.gradle` 에서 형제 디렉토리의 소스 프로젝트를 참조합니다.
|
||||||
- `WebContent/WEB-INF/properties/env.T.properties` - 테스트/스테이징 환경
|
|
||||||
- `WebContent/WEB-INF/properties/env.P.properties` - 운영 환경
|
|
||||||
|
|
||||||
|
| 모듈 | 위치 | 설명 |
|
||||||
|
|---|---|---|
|
||||||
|
| elink-online-core | `../eapim-online/elink-online-core` | 핵심 인터페이스 / 도메인 모델 |
|
||||||
|
| elink-online-core-jpa | `../eapim-online/elink-online-core-jpa` | JPA 엔티티 / 리포지토리 |
|
||||||
|
| elink-online-transformer | `../eapim-online/elink-online-transformer` | 메시지 변환 로직 |
|
||||||
|
| elink-online-common | `../eapim-online/elink-online-common` | 공통 유틸리티 |
|
||||||
|
| elink-online-emsclient | `../eapim-online/elink-online-emsclient` | EMS 통신 클라이언트 |
|
||||||
|
| elink-portal-common | `../elink-portal-common` | 포털 공통 컴포넌트 (JPA 엔티티) |
|
||||||
|
|
||||||
### 주의사항
|
> `kjb-safedb`, `eapim-admin-djb` 는 `settings.gradle` 에 주석 처리되어 있으며,
|
||||||
- **SafeDB**: 암호화 기능이 필요한 경우 `-Dkjb_safedb.mode=fake` 설정 (개발 환경) 또는 실제 SafeDB 라이브러리 설치
|
> 필요 시 주석을 해제하고 해당 디렉토리를 체크아웃합니다.
|
||||||
- **멀티 모듈 구조**: eapim-online의 여러 모듈들을 참조하므로 반드시 eapim-online도 클론해야 합니다
|
|
||||||
|
|
||||||
|
|
||||||
## 빌드 및 실행
|
## 빌드 및 실행
|
||||||
|
|
||||||
### Gradle 명령어
|
|
||||||
|
|
||||||
프로젝트 루트의 Gradle wrapper(`./gradlew`) 를 사용합니다.
|
프로젝트 루트의 Gradle wrapper(`./gradlew`) 를 사용합니다.
|
||||||
|
|
||||||
### 빌드 및 실행 명령어
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 표준 빌드
|
# 표준 빌드
|
||||||
./gradlew build
|
./gradlew build
|
||||||
|
|
||||||
# Weblogic 배포용 빌드 (테스트 제외)
|
|
||||||
./gradlew build -x test -Pprofile=weblogic
|
|
||||||
|
|
||||||
# WAR 파일 빌드[ems_stdout.log](../../logs/emsSvr-RinjaeMA/ems_stdout.log)
|
|
||||||
./gradlew war
|
|
||||||
|
|
||||||
# 클린 빌드
|
# 클린 빌드
|
||||||
./gradlew clean build
|
./gradlew clean build
|
||||||
|
|
||||||
# 테스트 실행
|
# WAR 빌드 (build/libs/eapim-admin.war)
|
||||||
./gradlew test
|
./gradlew war
|
||||||
|
|
||||||
# 패키징 없이 클래스만 컴파일
|
# WebLogic 배포용 빌드 (테스트 제외, weblogic-web.xml 사용)
|
||||||
|
./gradlew build -x test -Pprofile=weblogic
|
||||||
|
|
||||||
|
# 패키징 없이 컴파일만
|
||||||
./gradlew classes
|
./gradlew classes
|
||||||
|
|
||||||
# QueryDSL Q-classes 및 기타 애노테이션 생성
|
# QueryDSL Q-class 등 생성 코드만 생성
|
||||||
./gradlew compileJava
|
./gradlew compileJava
|
||||||
|
|
||||||
# 의존성 트리 보기
|
# 전체 테스트
|
||||||
./gradlew dependencies
|
./gradlew test
|
||||||
|
|
||||||
# 사용 가능한 모든 태스크 목록
|
# 특정 테스트
|
||||||
|
./gradlew test --tests "com.eactive.eai.rms.*"
|
||||||
|
|
||||||
|
# 의존성 트리 / 태스크 목록
|
||||||
|
./gradlew dependencies
|
||||||
./gradlew tasks --all
|
./gradlew tasks --all
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- WAR 파일명: `eapim-admin.war`, 컨텍스트 경로: `/monitoring`
|
||||||
|
- 프로필: `-Pprofile=weblogic` 지정 시 `web.xml` 대신 `weblogic-web.xml` 사용 (DefaultServlet 문제 회피)
|
||||||
|
|
||||||
|
## 로컬 개발 빠른 시작
|
||||||
|
|
||||||
|
1. **생성 코드 생성** - 최초 1회 및 pull 이후
|
||||||
|
```bash
|
||||||
|
./gradlew compileJava # Q-class IDE 오류 시: ./gradlew clean compileJava
|
||||||
|
```
|
||||||
|
2. **빌드/테스트**
|
||||||
|
```bash
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
3. **IDE 열기** - 아래 "IDE 설정" 참조
|
||||||
|
4. **WAS(Tomcat 등) 실행 구성** 에 `eapim-admin` 배포 + 아래 VM 옵션 추가 후 기동
|
||||||
|
|
||||||
|
### 로컬 Tomcat VM 옵션 예시
|
||||||
|
|
||||||
|
전체 목록과 설명은 `CLAUDE.md` 의 "필수 환경 변수 (Tomcat)" 를 참조하세요.
|
||||||
|
|
||||||
|
```
|
||||||
|
-Deai.datasource.type=DEV
|
||||||
|
-Dinst.Name=emsSvr11
|
||||||
|
-Deai.tableowner=EMSADM
|
||||||
|
-Deai.systemmode=D
|
||||||
|
-Dfile.encoding=utf-8
|
||||||
|
-Dlogin.mode=db
|
||||||
|
-DLOGBACK_LOG_LEVEL=DEBUG
|
||||||
|
-Ddamo-manager.enabled=true
|
||||||
|
-Dlogging.log-path=/logs/prod/eapim/emsSvr11
|
||||||
|
-Dlogback.configurationFile=classpath:logback-dev.xml
|
||||||
|
-Dhibernate.dialect=org.hibernate.dialect.Oracle12cDialect
|
||||||
|
```
|
||||||
|
|
||||||
|
- `inst.Name` 은 클러스터 내에서 인스턴스마다 고유해야 합니다.
|
||||||
|
- `eai.tableowner` 는 Oracle 스키마 소유자.
|
||||||
|
|
||||||
## 프로젝트 구조
|
## 프로젝트 구조
|
||||||
|
|
||||||
### 전체 프로젝트 구조
|
|
||||||
```
|
|
||||||
kjb-eapim/
|
|
||||||
├── eapim-admin/ # Admin 관리 콘솔 (현재 프로젝트)
|
|
||||||
├── eapim-online/ # Online 게이트웨이 코어 모듈
|
|
||||||
│ ├── elink-online-core/
|
|
||||||
│ ├── elink-online-core-jpa/
|
|
||||||
│ ├── elink-online-transformer/
|
|
||||||
│ ├── elink-online-common/
|
|
||||||
│ ├── elink-online-emsclient/
|
|
||||||
│ └── elink-online-adapter/
|
|
||||||
├── elink-portal-common/ # 포털 공통 컴포넌트 (JPA 엔티티)
|
|
||||||
├── kjb-safedb/ # SafeDB 암호화 라이브러리
|
|
||||||
└── eapim-portal/ # Portal 개발자 포털 (선택사항)
|
|
||||||
```
|
|
||||||
|
|
||||||
### eapim-admin 내부 구조
|
|
||||||
```
|
```
|
||||||
src/main/java/com/eactive/eai/
|
src/main/java/com/eactive/eai/
|
||||||
├── agent/command - 커맨드 패턴 구현
|
├── agent/command - 커맨드 패턴 구현
|
||||||
├── common/ - 공통 유틸리티, iBatis, JSON 직렬화
|
├── common - 공통 유틸리티, iBatis, JSON 직렬화
|
||||||
├── custom/ - 고객사별 커스터마이제이션
|
├── custom - 고객사별 커스터마이제이션
|
||||||
├── rms/ - 메인 애플리케이션 코드
|
├── rms - 메인 애플리케이션 코드
|
||||||
│ ├── bap/ - 배치 처리 (BAP)
|
│ ├── bap - 배치 처리(BAP): adaptor / manage / tansaction
|
||||||
│ ├── bat/ - 추가 배치 기능
|
│ ├── bat - 추가 배치 기능
|
||||||
│ ├── common/ - RMS 공통 (필터, 시작, 서비스)
|
│ ├── common - RMS 공통 (필터, 시작, 서비스)
|
||||||
│ ├── data/ - 데이터 엔티티, 리포지토리
|
│ ├── data - 데이터 엔티티, 리포지토리
|
||||||
│ ├── env/ - 환경 설정
|
│ ├── env - 환경 설정
|
||||||
│ ├── kakao/ - 카카오 연동
|
│ ├── kakao - 카카오 연동
|
||||||
│ ├── onl/ - 온라인 트랜잭션 관리
|
│ ├── onl - 온라인 트랜잭션 관리 (apim / manage / transaction)
|
||||||
│ └── service/ - 비즈니스 서비스
|
│ ├── service - 비즈니스 서비스
|
||||||
└── ext.kjb/ - 광주은행(KJB) 확장
|
│ └── ext
|
||||||
```
|
│ ├── djb - 제주은행(DJB) 확장 (webhook, UMS 연동, 통계 등)
|
||||||
|
│ └── kjb - 광주은행(KJB) 유래 확장
|
||||||
|
└── (com.eactive.ext.kjb) - 통계 화면 등 일부 확장 코드
|
||||||
|
|
||||||
### 웹 리소스 구조
|
|
||||||
```
|
|
||||||
WebContent/
|
WebContent/
|
||||||
├── WEB-INF/
|
├── WEB-INF/
|
||||||
│ ├── applicationContext.xml - Root Spring 설정
|
│ ├── applicationContext.xml - Root Spring 설정
|
||||||
│ ├── springapp-servlet.xml - Servlet Spring 설정
|
│ ├── springapp-servlet.xml - Servlet Spring 설정
|
||||||
│ ├── web.xml - Tomcat용 배포 디스크립터
|
│ ├── web.xml / weblogic-web.xml
|
||||||
│ ├── weblogic-web.xml - WebLogic용 배포 디스크립터
|
│ └── properties/ - env.D / env.T / env.P / env.L .properties
|
||||||
│ └── properties/ - 환경별 프로퍼티
|
├── jsp/ - 화면
|
||||||
│ ├── env.D.properties - 개발
|
└── guide/ - 사용자 가이드(docsify)
|
||||||
│ ├── env.T.properties - 테스트
|
|
||||||
│ └── env.P.properties - 운영
|
|
||||||
└── guide/ - 사용자 가이드
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 주요 기능
|
## 데이터 액세스 (하이브리드 ORM)
|
||||||
|
|
||||||
다음과 같은 관리 기능을 수행하며, 모든 관리 대상은 실시간으로 DB, 메모리에 있는 정보가 수정, 반영됩니다.
|
JPA 와 iBATIS 를 함께 사용합니다.
|
||||||
|
|
||||||
| 기능 | 설명 |
|
- **신규 기능**: JPA + Spring Data 리포지토리 (`com.eactive.eai.rms.data.entity`, `elink-portal-common`)
|
||||||
| -------------------- | ------------------------------------------------------------ |
|
- **레거시**: iBATIS SQL 매퍼 `src/main/resources/com/eactive/eai/**/*.xml` (Spring XML 설정, 점진적 마이그레이션 대상)
|
||||||
| 사용자 관리 | 사용자 및 권한을 관리하는 기능 |
|
- 벤더별 매핑: Oracle `*-oracle.xml`, MariaDB `*-mariadb.xml`, PostgreSQL `*-postgresql.xml`
|
||||||
| 로그 조회 | 거래 수행 내역을 조회 하는 기능(온라인/배치/일괄) |
|
|
||||||
| 통계 조회 | 거래 통계 조회 기능 |
|
|
||||||
| 대시보드 조회 | 거래 내역 및 장애상황을 모니터링 할 수 있는 대시보드 조회 기능 |
|
|
||||||
| 인터페이스 관리 | 인터페이스 등록/관리 기능 |
|
|
||||||
| 레이아웃 관리 | 레이아웃 등록/관리 기능 |
|
|
||||||
| 변환 관리 | 변환 등록/관리 기능 |
|
|
||||||
| 어댑터 관리 | 통신 어댑터 등록/관리 기능 |
|
|
||||||
| 라우팅룰 관리 | 라우팅 룰 등록/관리 기능 |
|
|
||||||
| 배치 어댑터 관리 | 배치 어댑터 등록/관리 기능 |
|
|
||||||
| 배치 인터페이스 관리 | 배치 인터페이스 등록/관리 기능 |
|
|
||||||
| 일괄 관리 | 일괄 등록/관리 기능 |
|
|
||||||
|
|
||||||
## 외부 모듈
|
### Multi-tenancy (런타임 스키마 전환)
|
||||||
|
|
||||||
이 프로젝트는 다음 모듈에 의존합니다 (settings.gradle 참조):
|
Hibernate Multi-tenancy 로 요청 시점에 스키마를 전환합니다.
|
||||||
|
`DataSourceContextHolder`(ThreadLocal) → `TenantIdentifierResolver` → `ConfigurableMultiTenantConnectionProvider.setSchema()`.
|
||||||
| 이름 | 버전 | 설명 |
|
요청 파라미터 `serviceType`(예: `APIGW`) 를 `DataSourceTypeInterceptor` 가 읽어 자동 설정하거나, 컨트롤러에서 명시 설정합니다.
|
||||||
| ------------------------ | ------------ | ---------------------------------------------- |
|
자세한 내용은 `CLAUDE.md` 의 "Multi-tenancy" 절 참조.
|
||||||
| elink-online-core | 4.5-SNAPSHOT | 코어 모듈로 인터페이스 클래스 정의 |
|
|
||||||
| elink-online-core-jpa | 4.5-SNAPSHOT | JPA 엔티티 및 리포지토리 |
|
|
||||||
| elink-online-transformer | 4.5-SNAPSHOT | 변환 모듈로 메시지 및 변환 관련 클래스 정의 |
|
|
||||||
| elink-online-adapter | 4.5-SNAPSHOT | 어댑터 모듈로 통신에 필요한 어댑터 클래스 정의 |
|
|
||||||
| elink-online-common | 4.5-SNAPSHOT | 나머지 공통 프로그램이 정의되어있다 |
|
|
||||||
| elink-online-emsclient | 4.5-SNAPSHOT | ems와 통신하기 위한 모듈 |
|
|
||||||
| elink-portal-common | - | 포털 공통 컴포넌트 (JPA 엔티티) |
|
|
||||||
| kjb-safedb | - | 커스텀 암호화 라이브러리 |
|
|
||||||
| eapim-admin-kjb | - | KJB 은행 특화 커스터마이제이션 |
|
|
||||||
|
|
||||||
## 하이브리드 ORM 방식
|
|
||||||
|
|
||||||
이 애플리케이션은 **JPA와 iBATIS 모두를 사용**합니다:
|
|
||||||
- **JPA/Hibernate**: `elink-portal-common`을 통한 현대적인 데이터 액세스 (Spring Data repositories)
|
|
||||||
- **iBATIS**: 기존 코드를 위한 레거시 SQL 매핑 (점진적으로 마이그레이션 예정)
|
|
||||||
|
|
||||||
데이터 액세스 작업 시:
|
|
||||||
- 새로운 기능은 JPA와 Spring Data repositories를 사용해야 합니다
|
|
||||||
- 레거시 iBATIS 매퍼는 `src/main/resources/com/eactive/eai/**/*.xml`에 있으며 Spring XML을 통해 설정됩니다
|
|
||||||
|
|
||||||
## 코드 생성
|
## 코드 생성
|
||||||
|
|
||||||
빌드 프로세스에서 생성되는 것들:
|
빌드 시 생성됩니다.
|
||||||
- **QueryDSL Q-classes**: 타입 안전 쿼리 클래스
|
|
||||||
- **Lombok**: 애노테이션 프로세싱을 통한 Getters/setters/builders
|
|
||||||
- **MapStruct**: DTO와 엔티티 간의 객체 매퍼
|
|
||||||
|
|
||||||
### 이중 Q-Class 생성 (Gradle + Eclipse)
|
|
||||||
|
|
||||||
이 프로젝트는 **Gradle과 Eclipse 애노테이션 프로세서 모두를 사용**하므로, Q-classes가 **두 위치에 생성**됩니다:
|
|
||||||
|
|
||||||
1. **`build/generated/java/`** - `gradle compileJava` 또는 `gradle build` 실행 시 Gradle이 생성
|
|
||||||
2. **`WebContent/generated/`** - Eclipse IDE에서 빌드 시 Eclipse APT가 생성
|
|
||||||
|
|
||||||
**이는 예상된 동작이며 의도적입니다.** 두 디렉토리 모두 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다.
|
|
||||||
|
|
||||||
## AI 코딩 어시스턴트 지침 파일
|
|
||||||
|
|
||||||
이 프로젝트는 여러 AI 코딩 어시스턴트를 지원합니다. 각 도구는 다음 파일을 참조합니다:
|
|
||||||
|
|
||||||
### 지침 파일 위치
|
|
||||||
|
|
||||||
| AI 도구 | 지침 파일 경로 | 용도 |
|
|
||||||
|---------|---------------|------|
|
|
||||||
| **Claude Code** | `CLAUDE.md` | 마스터 지침 파일 (한글) |
|
|
||||||
| **GitHub Copilot** | `.github/copilot-instructions.md` | Copilot용 지침 |
|
|
||||||
| **Cursor** | `.cursorrules` | Cursor용 간결한 규칙 |
|
|
||||||
|
|
||||||
### 지침 파일 관리 방법
|
|
||||||
|
|
||||||
1. **마스터 파일**: `CLAUDE.md`
|
|
||||||
- 가장 상세한 프로젝트 지침을 포함
|
|
||||||
- 한글로 작성되어 한국 개발팀이 이해하기 쉬움
|
|
||||||
- 수정 시 이 파일을 먼저 업데이트
|
|
||||||
|
|
||||||
2. **동기화**:
|
|
||||||
```bash
|
|
||||||
# CLAUDE.md 수정 후 다른 파일들 동기화
|
|
||||||
cp CLAUDE.md .github/copilot-instructions.md
|
|
||||||
# .cursorrules는 간결한 버전이므로 필요시 수동 업데이트
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **새로운 AI 도구 지원 추가**:
|
|
||||||
- Windsurf: `.windsurfrules` 파일 생성
|
|
||||||
- 기타 도구: 해당 도구의 규칙 파일명 확인 후 생성
|
|
||||||
|
|
||||||
|
- **QueryDSL Q-class** - `build/generated/java/` 에 생성, `.gitignore` 대상 (커밋 금지)
|
||||||
|
- **Lombok** - getter/setter/builder 등
|
||||||
|
- **MapStruct** - DTO ↔ 엔티티 매퍼
|
||||||
|
|
||||||
|
pull 이후 또는 Q-class IDE 오류 시 `./gradlew compileJava` (또는 `clean compileJava`) 로 재생성합니다.
|
||||||
|
|
||||||
## 환경 설정
|
## 환경 설정
|
||||||
|
|
||||||
### 시스템 모드
|
### 시스템 모드
|
||||||
|
|
||||||
- **D (DEV)**: 개발 환경
|
`-Deai.systemmode` 로 지정하며 해당 프로퍼티 파일을 로드합니다.
|
||||||
- **T (TEST)**: 테스트/스테이징 환경
|
|
||||||
- **P (PROD)**: 운영 환경
|
|
||||||
|
|
||||||
시스템 모드는 `-Deai.systemmode` JVM 옵션으로 설정하며, 해당 환경의 프로퍼티 파일(`env.D.properties` 등)을 로드합니다.
|
| 값 | 환경 | 파일 |
|
||||||
|
|---|---|---|
|
||||||
|
| D | 개발 | `env.D.properties` |
|
||||||
|
| T | 테스트 / 스테이징 | `env.T.properties` |
|
||||||
|
| P | 운영 | `env.P.properties` |
|
||||||
|
| L | 로컬 | `env.L.properties` |
|
||||||
|
|
||||||
### TOMCAT 기동 옵션 참조
|
### 데이터베이스
|
||||||
|
|
||||||
#### 은행 개발 서버용
|
- **Oracle**: 운영 주 DB (`hibernate.dialect=org.hibernate.dialect.Oracle12cDialect`)
|
||||||
```
|
- **MariaDB / PostgreSQL**: 벤더별 SQL 매핑으로 지원 (JDBC 의존성은 필요 시 활성화)
|
||||||
-Deai.datasource.type=DEV
|
- **H2**: 테스트용 인메모리 DB (`src/test/resources`)
|
||||||
-Dinst.Name=emsSvr11
|
- 연결 타입은 `-Deai.datasource.type`(DEV/STG/PROD) 로 선택
|
||||||
-Deai.tableowner=EMSAPP
|
|
||||||
-Deai.systemmode=D
|
|
||||||
-Dfile.encoding=utf-8
|
|
||||||
-DLOGBACK_LOG_LEVEL=info
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 로컬 개발용
|
### DB 암호화
|
||||||
```
|
|
||||||
-Deai.datasource.type=DEV
|
`damo-manager.jar` 모듈은 환경에 따라 다른 암호화로직을 제공한다.
|
||||||
-Dinst.Name=emsSvr99
|
|
||||||
-Deai.tableowner=EMSAPP
|
| 환경 | 설명 |
|
||||||
-Deai.systemmode=D
|
|---|---|
|
||||||
-Dfile.encoding=utf-8
|
| 운영 | 실제 D'amo 네이티브 라이브러리 |
|
||||||
-Dlogin.mode=db
|
| 개발 | SHA-256 |
|
||||||
-DLOGBACK_LOG_LEVEL=DEBUG
|
|
||||||
-Dlogback.configurationFile=classpath:logback-dev.xml
|
로컬 개발 시 `-Ddamo-manager.enabled=false`
|
||||||
-Dhibernate.dialect=org.hibernate.dialect.Oracle12cDialect
|
|
||||||
-Dkjb_safedb.mode=fake
|
## DJB (제주은행) 전용 설정
|
||||||
|
|
||||||
|
DJB 전용 프로퍼티는 DB 테이블 `TSEAIRM24` 에 그룹명 `'Monitoring'` 과
|
||||||
|
DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다.
|
||||||
|
|
||||||
|
> Monitoring 프라퍼티 (환경정보 > EMS관리 > 모니터링 프라퍼티)
|
||||||
|
|
||||||
|
**계정관리**
|
||||||
|
- `djb.sms_auth.enabled` - 로그인시 2-factor 인증 적용여부
|
||||||
|
- `djb.sms_auth.fixed_value` - sms_auth.mode=fixed일 경우 고정인증값
|
||||||
|
- `djb.sms_auth.mode` - 2-factor 인증 모드 (real:실제, fixed:고정값-개발환경에서 사용)
|
||||||
|
- `rms.DUAL_LOGIN_ENABLED` - 중복 로그인 허용 여부
|
||||||
|
- `rms.auto.logout.timeout` - 자동 로그아웃 시간(분)
|
||||||
|
- `rms.password.combi.check` - 비밀번호 영문/숫자/특수문자 조합 종류수
|
||||||
|
- `rms.password.fail.count` - 비밀번호 실패 허용 횟수(이 값 이상 실패시 계정 잠김)
|
||||||
|
- `rms.password.history.count` - 이전 비밀번호 변경 불가 횟수
|
||||||
|
- `rms.password.init.subfix` - 관리자가 비밀번호 초기화시 추가 문자 ex) 행번@!
|
||||||
|
- `rms.password.length.check` - 비밀번호 최소 자리수
|
||||||
|
- `rms.password.repeat.check` - 비밀번호 동일문자,연속문자 반복 불가 횟수
|
||||||
|
- `rms.password.sms_auth.enabled` - 비밀번호 변경시 2-factor 인증 여부
|
||||||
|
|
||||||
|
**인터페이스 배포**
|
||||||
|
- `iomap.download.path` - 인터페이스.json 파일 다운로드 서버 경로
|
||||||
|
- `iomap.upload.path` - 인터페이스.json 파일 업로드 서버 경로
|
||||||
|
|
||||||
|
**UMS(공통) 연동**
|
||||||
|
- `djb.ums.was_ip_address` - 관리자포탈 WAS IP 주소 (UMS 연동시 필요)
|
||||||
|
- `djb.ums.was_mac_address` - 관리자포탈 WAS MAC 주소 (UMS 연동 시 필요)
|
||||||
|
|
||||||
|
**UMS(이메일) 연동**
|
||||||
|
- `djb.ums.email.url` - 이메일 API 엔드포인트 (EAI)
|
||||||
|
- `djb.ums.email.cstno` - 고객번호
|
||||||
|
- `djb.ums.email.ums_evnt_id` - UMS이벤트 ID
|
||||||
|
|
||||||
|
**UMS(사내메신저) 연동**
|
||||||
|
- `djb.ums.messenger.url` - 사내메신저 API 앤드포인트 (FEP)
|
||||||
|
- `djb.ums.messenger.client_id` - 사내메신저 클라이언트 아이디
|
||||||
|
- `djb.ums.messenger.client_secret` - 사내메신저 클라이언트 시크릿
|
||||||
|
- `djb.ums.messenger.api-monitor.enabled` - API 상태변화시 직원에게 사내메신저 발송 여부
|
||||||
|
|
||||||
|
**UMS(SMS) 연동**
|
||||||
|
- `djb.ums.sms.url` - SMS API 엔드포인트 (EAI)
|
||||||
|
- `djb.ums.sms.almtk_snd_prtl_key.intra` - 카톡 알림톡 채널 ID (제주은행 임직원)
|
||||||
|
- `djb.ums.sms.almtk_snd_prtl_key.public` - 카톡 알림톡 채널 ID (제주은행)
|
||||||
|
|
||||||
|
**Webhook 발송**
|
||||||
|
- `rms.webhook.enabled` - 웹훅 발송 여부
|
||||||
|
- `rms.webhook.retry_count` - 웹훅 재발송 횟수
|
||||||
|
- `rms.webhook.retry_time` - 웹훅 재발송 시간간격
|
||||||
|
- `rms.webhook.reverse_proxy.url` - 리버스 프록시 경로
|
||||||
|
|
||||||
|
|
||||||
-Dlogback.configurationFile="C:\eactive\workspaces\kjb-eapim\eapim-admin\src\main\resources\logback-rinjae.xml"
|
> Portal 프라퍼티 (파트너포탈 > 포탈관리 > Property관리)
|
||||||
```
|
|
||||||
|
|
||||||
### 데이터베이스 지원
|
**UMS 연동**
|
||||||
|
- `djb.ums.email.if_id` - 이메일 연동 인터페이스 ID
|
||||||
|
- `djb.ums.email.tx_id` - 이메일 거래 ID
|
||||||
|
- `djb.ums.messenger.if_id` - 사내메신저 인터페이스 ID
|
||||||
|
- `djb.ums.messenger.tx_id` - 사내메신저 거래 ID
|
||||||
|
- `djb.ums.sms.if_id` - SMS 인터페이스 ID
|
||||||
|
- `djb.ums.sms.tx_id` - SMS 거래 ID
|
||||||
|
|
||||||
- **Oracle**: 운영 데이터베이스 (주)
|
|
||||||
- **PostgreSQL/MariaDB**: 대체 운영 DB
|
|
||||||
- **H2**: 테스트용 인메모리 데이터베이스
|
|
||||||
|
|
||||||
데이터베이스 벤더는 프로퍼티 파일의 `db.vendor`와 `hibernate.dialect`를 통해 설정됩니다.
|
## 스케쥴러
|
||||||
|
|
||||||
### SafeDB 암호화 라이브러리
|
**API 상태 모니터링**
|
||||||
|
- `ApiStatusMonitorJob11, ApiStatusMonitorJob21` - API 상태 변화를 모니터링한다. API 상태 변화 발생시 직원에게 메신저 및 제휴사에게 웹훅을 발송한다.
|
||||||
|
- 실행간격(1분)이 짧아서 CLUSTERED는 적용을 못하고, 서버수만큼 등록하여 번갈아 가면서 실행하도록 설정
|
||||||
|
- `api.status.error.range_minute` - API 장애 판단을 위한 로그 구간(분)
|
||||||
|
- `api.status.error.rate` - API 장애판단을 위한 오류율 (ex : 10분동안 90% 이상 오류 발생시 장애로 판단)
|
||||||
|
- `api.status.delay.range_minute` - API 지연 상태 판단을 위한 로그 구간(분)
|
||||||
|
- `api.status.delay.avg_resp_time` - API 지연 상태 판단을 위한 평균 응답속도 (ex: 10분동안 평균 응답속도가 10초이상이면 지연으로 판단)
|
||||||
|
|
||||||
`kjb-safedb` 모듈은 세 가지 동작 모드를 제공합니다:
|
**유량제어토큰실패감시**
|
||||||
- **REAL**: 실제 SafeDB 네이티브 라이브러리 사용
|
- `InflowTokenMonitorJob` - 유량제어 토큰 획득 실패(TSEAIFR11) 발생시 직원에게 메신저를 발송한다
|
||||||
- **FAKE**: SHA-256/AES-256을 사용하는 개발 모드 (SafeDB 설치 불필요)
|
- `api.inflow.fail.range_minute` - 최근 몇분동안 감시하는지 설정
|
||||||
- **NONE**: 암호화 우회 (테스트 전용)
|
|
||||||
|
|
||||||
개발 시 SafeDB가 로컬에 설치되지 않은 경우 `-Dkjb_safedb.mode=fake`를 설정합니다.
|
**API 통계**
|
||||||
|
- `ApiStatsHourlyAggregationJob` - 당일 API 로그 정보를 시간별로 집계 (TSEAILG00 > API_STATS_HOUR)
|
||||||
|
- `APISTATSDAILYJOB` - API 사용통계 일집계 (API_STATS_HOUR > API_STATS_DAY)
|
||||||
|
- `APISTATSMONTHLYJOB` - API 사용통계 월집계 (API_STATS_DAY > API_STATS_MONTH)
|
||||||
|
- `APISTATSYEARLYJOB` - API 사용통계 년집계 (API_STATS_MONTH > API_STATS_YEAR)
|
||||||
|
|
||||||
|
**UMS발송**
|
||||||
|
- `UMSDISPATCHJOB11, UMSDISPATCHJOB21` - UMS를 발송한다.
|
||||||
|
- 실행간격(5초)이 짧아서 CLUSTERED는 적용을 못하고, 서버수만큼 등록하여 번갈아 가면서 실행하도록 설정
|
||||||
|
|
||||||
|
**게시글종료**
|
||||||
|
- `PortalInquiryClosingJob` - 답변완료 이후 일정기간 댓글이 없는 경우 게시글을 종료한다
|
||||||
|
- `inquiry.comment.closing_day` - 게시글 종료 대기일수
|
||||||
|
|
||||||
|
**메모리초기화**
|
||||||
|
- `MEMORYTRINIT` - 관리자포탈 서버 메모리 초기화(G/W에서 받은 거래현황)
|
||||||
|
|
||||||
|
**토큰발급이력삭제**
|
||||||
|
- `TOKENISSUANCELOGCLEANUPJOB` - 7일이 지난 토큰 발급내역을 삭제한다
|
||||||
|
|
||||||
## 배포
|
## 배포
|
||||||
|
|
||||||
### 지원 서버
|
| 대상 | 비고 |
|
||||||
- **Tomcat** (기본, 개발)
|
|---|---|
|
||||||
- **WebLogic 14.1.2** (운영, `-Pprofile=weblogic` 필요)
|
| Tomcat | 기본, 개발 |
|
||||||
- **JEUS** (지원)
|
| WebLogic 14.1.2 | 운영, `-Pprofile=weblogic` 필요 |
|
||||||
- **JBoss/WildFly** (descriptors와 함께 지원)
|
| JEUS | 지원 |
|
||||||
|
| JBoss / WildFly | descriptor 포함 지원 |
|
||||||
|
|
||||||
### WebLogic 전용 빌드
|
- 모든 관리 변경사항은 DB 와 메모리에 즉시 반영됩니다.
|
||||||
|
- 인코딩은 UTF-8. `CharacterEncodingFilter` 가 `*.excel` 을 제외한 모든 요청에 적용.
|
||||||
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
|
- XSS 보호: `com.eactive.eai.rms.common.filter.CrossScriptingFilter` 전역 적용.
|
||||||
|
|
||||||
```bash
|
|
||||||
gradle build -x test -Pprofile=weblogic
|
|
||||||
```
|
|
||||||
|
|
||||||
### WAR 배포
|
|
||||||
|
|
||||||
WAR 파일은 context path `/monitoring`으로 `eapim-admin.war`로 빌드됩니다.
|
|
||||||
|
|
||||||
## IDE 설정
|
## IDE 설정
|
||||||
|
|
||||||
### Eclipse
|
`build.gradle` 은 `eclipse` / `eclipse-wtp` / `idea` 플러그인을 모두 포함합니다.
|
||||||
|
IntelliJ 전용 변형은 `build.gradle.intellij` 로 별도 관리합니다 (필요 시 `build.gradle` 로 교체).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gradle eclipse
|
# Eclipse
|
||||||
|
./gradlew eclipse # .project / .classpath, WTP(context path: /monitoring), APT 설정 생성
|
||||||
|
|
||||||
|
# IntelliJ IDEA
|
||||||
|
./gradlew idea # 또는 IDE 에서 "Import Gradle Project"
|
||||||
```
|
```
|
||||||
|
|
||||||
다음을 생성합니다:
|
|
||||||
- `.project` 및 `.classpath` 파일
|
|
||||||
- Eclipse WTP 설정 (context path: `/monitoring`)
|
|
||||||
- UTF-8 인코딩 설정
|
|
||||||
- QueryDSL/Lombok을 위한 애노테이션 프로세서 설정
|
|
||||||
|
|
||||||
### IntelliJ IDEA
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gradle idea
|
|
||||||
```
|
|
||||||
|
|
||||||
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
|
|
||||||
|
|
||||||
## KJB (광주은행) 전용 설정
|
|
||||||
|
|
||||||
### 프로퍼티 관리
|
|
||||||
KJB 전용 프로퍼티는 데이터베이스 테이블 `TSEAIRM24`에 그룹명 `'Monitoring'`으로 저장됩니다.
|
|
||||||
|
|
||||||
|
|
||||||
### 주요 프로퍼티 설정
|
|
||||||
|
|
||||||
**이메일 알림 (EAI Batch를 통해)**
|
|
||||||
- `kjb.eai_batch.url` - 이메일 발송을 위한 EAI 배치 서버 URL
|
|
||||||
- 개발: http://172.31.32.111:10230/BATAppWeb/EaiBatCall
|
|
||||||
- QA: http://172.31.33.111:10430/BATAppWeb/EaiBatCall
|
|
||||||
- 운영: http://172.21.1.40:10860/BATAppWeb/EaiBatCall
|
|
||||||
- 입력하지 않을 경우 EAI 배치 호출을 하지 않고 내부적으로 통신하지 않으며 warning 로그만 남김
|
|
||||||
- `kjb.ums.eai_batch.send_dir` - 이메일 파일 발송 디렉토리 (기본값: `/Data/eapim/portal/sendmail/snd`)
|
|
||||||
- `kjb.ums.eai_batch.backup_dir` - 이메일 백업 디렉토리 (기본값: `/Data/eapim/portal/sendmail/bak`)
|
|
||||||
- `kjb.ums.eai_batch.interface_id` - 고객 이메일용 인터페이스 ID (예: `UAGFF00001UIE`)
|
|
||||||
|
|
||||||
**UMS (SMS/LMS/카카오톡) 연동**
|
|
||||||
- `kjb.ums.host_url` - UMS API 엔드포인트
|
|
||||||
- `kjb.ums.api_key` - UMS API 인증 키
|
|
||||||
- `kjb.ums.connection_timeout` - 연결 타임아웃 (초 단위)
|
|
||||||
- `kjb.ums.response_timeout` - 응답 타임아웃 (초 단위)
|
|
||||||
|
|
||||||
### 보안
|
|
||||||
- 커스텀 패스워드 인코더: SafeDB 연동을 위한 `KjbSafedbPasswordEncoder`
|
|
||||||
|
|
||||||
## 테스트
|
## 테스트
|
||||||
|
|
||||||
테스트에 사용되는 것들:
|
- **JUnit 5 (Jupiter)** - 테스트 프레임워크
|
||||||
- **JUnit 5 (Jupiter)**: 테스트 프레임워크
|
- **Spring Boot Test 2.6.15** - 통합 테스트 유틸리티
|
||||||
- **Spring Boot Test 2.6.15**: 통합 테스트 유틸리티
|
- **H2** - 인메모리 DB (`src/test/resources`)
|
||||||
- **H2 Database**: 테스트용 인메모리 데이터베이스
|
- **Mockito** - 모킹 (Spring Boot Test 경유)
|
||||||
- **Mockito**: 모킹 프레임워크 (Spring Boot Test를 통해)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 모든 테스트 실행
|
./gradlew test
|
||||||
gradle test
|
./gradlew test --tests "com.eactive.eai.rms.*"
|
||||||
|
|
||||||
# 특정 테스트 클래스 실행
|
|
||||||
gradle test --tests "com.example.ClassName"
|
|
||||||
|
|
||||||
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
|
|
||||||
gradle test --tests "com.eactive.eai.rms.*"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 관련 프로젝트
|
|
||||||
|
|
||||||
프로젝트 구조 (kjb-eapim 디렉토리 기준):
|
|
||||||
- `eapim-admin/` - Admin 관리 콘솔 (현재 프로젝트)
|
|
||||||
- `eapim-online/` - Online 게이트웨이 (코어 모듈 포함)
|
|
||||||
- `eapim-portal/` - Portal 개발자 포털
|
|
||||||
- `elink-portal-common/` - 공통 유틸리티 라이브러리
|
|
||||||
- `kjb-safedb/` - SafeDB 암호화 라이브러리
|
|
||||||
- `kjb-eapim-sql/` - SQL 스크립트 (선택사항)
|
|
||||||
|
|
||||||
## 문서
|
## 문서
|
||||||
|
|
||||||
- **프로젝트 상세 지침**: `CLAUDE.md` (한글)
|
- **프로젝트 상세 지침**: `CLAUDE.md` (AI 어시스턴트 및 개발자용 마스터 문서, 한글)
|
||||||
- **사용자 가이드**: `WebContent/guide/` (한글)
|
- **사용자 가이드**: `WebContent/guide/` (docsify)
|
||||||
|
|
||||||
## 참고: KJB 데이터베이스 파라미터 설정
|
|
||||||
|
|
||||||
### 추가 파라미터 SQL
|
|
||||||
```sql
|
|
||||||
-- 로컬 개발용 (외부)
|
|
||||||
-- EAI, UMS 주소 미기입시 내부적으론 호출 시도 하지 않고 warning 로그만 남기며 정상 처리
|
|
||||||
--insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', '');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
|
|
||||||
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
|
|
||||||
|
|
||||||
-- insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', '');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
|
|
||||||
|
|
||||||
--insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', '');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
|
|
||||||
commit;
|
|
||||||
|
|
||||||
|
|
||||||
-- 은행 개발 서버용
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.31.32.111:10230/BATAppWeb/EaiBatCall');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.31.35.144:9021');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
|
|
||||||
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', 'http://192.168.246.15:8181');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
|
|
||||||
commit;
|
|
||||||
|
|
||||||
|
|
||||||
-- 은행 운영 서버용
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.21.1.40:10860/BATAppWeb/EaiBatCall');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.24.128.66:9021');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
|
|
||||||
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', 'http://192.168.246.15:8181');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
|
|
||||||
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
|
|
||||||
commit;
|
|
||||||
```
|
|
||||||
## 라이선스
|
## 라이선스
|
||||||
|
|
||||||
© 광주은행 (Kwangju Bank)
|
사내 전용(proprietary). 제주은행 eLink EMS.
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import com.eactive.eai.rms.common.util.CommonConstants;
|
|||||||
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusiness;
|
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusiness;
|
||||||
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessId;
|
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessId;
|
||||||
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessService;
|
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessService;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.transaction.TransactionTrackingFilter;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.transaction.TransactionTrackingFilterId;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.transaction.TransactionTrackingFilterService;
|
||||||
import com.eactive.eai.rms.data.entity.onl.unifbwk.UnifBwkTpService;
|
import com.eactive.eai.rms.data.entity.onl.unifbwk.UnifBwkTpService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -40,6 +43,9 @@ public class BizManService extends BaseService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserBusinessService userBusinessService;
|
private UserBusinessService userBusinessService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TransactionTrackingFilterService transactionTrackingFilterService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserBizUIMapper userBizUIMapper;
|
private UserBizUIMapper userBizUIMapper;
|
||||||
|
|
||||||
@@ -102,6 +108,13 @@ public class BizManService extends BaseService {
|
|||||||
|
|
||||||
userBusinessService.deleteOrSave(userId, userBusinesses);
|
userBusinessService.deleteOrSave(userId, userBusinesses);
|
||||||
|
|
||||||
|
transactionTrackingFilterService.deleteAllByUserId(userId);
|
||||||
|
List<TransactionTrackingFilter> filters = userBusinesses.stream()
|
||||||
|
.map(ub -> new TransactionTrackingFilter(
|
||||||
|
new TransactionTrackingFilterId(userId, ub.getId().getEaiBzwkDstcd())))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
transactionTrackingFilterService.saveAll(filters);
|
||||||
|
|
||||||
String userBusinessServiceLists = userBusinesses.stream()
|
String userBusinessServiceLists = userBusinesses.stream()
|
||||||
.map(e -> e.getId().getEaiBzwkDstcd())
|
.map(e -> e.getId().getEaiBzwkDstcd())
|
||||||
.collect(Collectors.joining(","));
|
.collect(Collectors.joining(","));
|
||||||
|
|||||||
@@ -273,6 +273,18 @@ public interface MonitoringContext {
|
|||||||
// 비밀번호 변경 시 재사용을 금지할 최근 이력 개수
|
// 비밀번호 변경 시 재사용을 금지할 최근 이력 개수
|
||||||
public static final String RMS_PASSWORD_HISTORY_COUNT = "rms.password.history.count";
|
public static final String RMS_PASSWORD_HISTORY_COUNT = "rms.password.history.count";
|
||||||
|
|
||||||
|
// 비밀번호 변경 시 SMS 2차 인증 사용 여부 (djb.sms_auth.enabled 와 함께 true 여야 동작)
|
||||||
|
public static final String RMS_PASSWORD_SMS_AUTH_ENABLED = "rms.password.sms_auth.enabled";
|
||||||
|
|
||||||
|
// 비밀번호 최소 길이 (기본 7)
|
||||||
|
public static final String RMS_PASSWORD_LENGTH_CHECK = "rms.password.length.check";
|
||||||
|
|
||||||
|
// 비밀번호 문자 조합 종류 수 (영문/숫자/특수문자 중 N종류 이상, 기본 2)
|
||||||
|
public static final String RMS_PASSWORD_COMBI_CHECK = "rms.password.combi.check";
|
||||||
|
|
||||||
|
// 동일 문자 연속 반복 제한 개수 (기본 3, 2 미만이면 미검사)
|
||||||
|
public static final String RMS_PASSWORD_REPEAT_CHECK = "rms.password.repeat.check";
|
||||||
|
|
||||||
// API 상태변화 스윙챗 발송여부
|
// API 상태변화 스윙챗 발송여부
|
||||||
public static final String DJB_UMS_MESSENGER_APIMONITOR_ENABLED = "djb.ums.messenger.api-monitor.enabled";
|
public static final String DJB_UMS_MESSENGER_APIMONITOR_ENABLED = "djb.ums.messenger.api-monitor.enabled";
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
|||||||
// src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과
|
// src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과
|
||||||
value = filterSrcAttribute(value);
|
value = filterSrcAttribute(value);
|
||||||
|
|
||||||
// 리치 텍스트 파라미터는 문자 변환을 건너뛰고 XSS 패턴 필터링만 적용
|
// 리치 텍스트 파라미터와 메시지 템플릿 저장 요청은 문자 변환 없이 XSS 패턴 필터링만 적용
|
||||||
if (!shouldSkipCharConvert()) {
|
if (!shouldSkipCharConvert()) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
value = convertChars(value, sb);
|
value = convertChars(value, sb);
|
||||||
@@ -179,6 +179,15 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
|||||||
if (param == null) {
|
if (param == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// 상세 화면은 .view로 열리지만 등록/수정 데이터는 .json으로 POST됨.
|
||||||
|
// 문자 발송에 사용되는 입력값이 HTML 엔티티로 저장되지 않도록 함.
|
||||||
|
if ("POST".equalsIgnoreCase(getMethod())
|
||||||
|
&& "/onl/apim/template/messageTemplateMan.json".equals(getServletPath())) {
|
||||||
|
String cmd = super.getParameter("cmd");
|
||||||
|
if ("INSERT".equals(cmd) || "UPDATE".equals(cmd)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (String skipParam : SKIP_CHAR_CONVERT_PARAMS) {
|
for (String skipParam : SKIP_CHAR_CONVERT_PARAMS) {
|
||||||
if (StringUtils.equalsIgnoreCase(param, skipParam)) {
|
if (StringUtils.equalsIgnoreCase(param, skipParam)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
package com.eactive.eai.rms.common.interceptor;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Map;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class ApiKeyInterceptor implements HandlerInterceptor {
|
|
||||||
|
|
||||||
private static final String API_KEY_HEADER = "X-API-KEY";
|
|
||||||
private static final String PORTAL_PROPERTY_GROUP = "Portal";
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private PortalPropertyService portalPropertyService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
|
||||||
try {
|
|
||||||
String requestApiKey = request.getHeader(API_KEY_HEADER);
|
|
||||||
|
|
||||||
if (StringUtils.isEmpty(requestApiKey)) {
|
|
||||||
log.warn("Request received without API key. URI: {}", request.getRequestURI());
|
|
||||||
sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "API key is missing");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
|
|
||||||
String validApiKey = properties.get("apiKey");
|
|
||||||
|
|
||||||
if (StringUtils.isEmpty(validApiKey)) {
|
|
||||||
log.error("API key not configured in properties");
|
|
||||||
sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "API key is not configured");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validApiKey.equals(requestApiKey)) {
|
|
||||||
log.warn("Invalid API key received. URI: {}", request.getRequestURI());
|
|
||||||
sendErrorResponse(response, HttpServletResponse.SC_FORBIDDEN, "Invalid API key");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error processing API key validation", e);
|
|
||||||
sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error processing request");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
|
|
||||||
response.setStatus(status);
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.getWriter().write(String.format("{\"error\": \"%s\"}", message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.eactive.eai.rms.common.interceptor;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.common.internal.InternalApiTokenService;
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /_onl/**} 내부 연동 요청을 공유 내부 토큰으로 검증한다.
|
||||||
|
*
|
||||||
|
* <p>토큰과 헤더명은 PTL_PROPERTY {@code Portal / internal.api.header-name},
|
||||||
|
* {@code Portal / internal.api.token}을 사용한다. admin은 토큰을 생성하지 않고 읽기 전용으로
|
||||||
|
* 검증하므로, portal이 먼저 기동되어 토큰을 생성해야 한다.</p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class InternalApiTokenInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
private final InternalApiTokenService internalApiTokenService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
|
||||||
|
String headerName = internalApiTokenService.findHeaderName();
|
||||||
|
String presentedToken = request.getHeader(headerName);
|
||||||
|
|
||||||
|
if (!internalApiTokenService.matchesReadOnly(presentedToken)) {
|
||||||
|
log.warn("Invalid internal API token. URI: {}, header: {}", request.getRequestURI(), headerName);
|
||||||
|
sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "Internal API token is invalid");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
|
||||||
|
response.setStatus(status);
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
response.getWriter().write(String.format("{\"error\": \"%s\"}", message));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,6 +67,9 @@ public class MainController implements InterceptorSkipController {
|
|||||||
private static final String USER_STATUS_LOCKED = "2";
|
private static final String USER_STATUS_LOCKED = "2";
|
||||||
private static final int DEFAULT_MAX_LOGIN_FAIL_COUNT = 5;
|
private static final int DEFAULT_MAX_LOGIN_FAIL_COUNT = 5;
|
||||||
private static final int DEFAULT_PASSWORD_HISTORY_COUNT = 5;
|
private static final int DEFAULT_PASSWORD_HISTORY_COUNT = 5;
|
||||||
|
private static final int DEFAULT_PASSWORD_LENGTH_CHECK = 7;
|
||||||
|
private static final int DEFAULT_PASSWORD_COMBI_CHECK = 2;
|
||||||
|
private static final int DEFAULT_PASSWORD_REPEAT_CHECK = 3;
|
||||||
|
|
||||||
private final LocaleMessage localeMessage;
|
private final LocaleMessage localeMessage;
|
||||||
private final MonitoringContext monitoringContext;
|
private final MonitoringContext monitoringContext;
|
||||||
@@ -220,7 +223,7 @@ public class MainController implements InterceptorSkipController {
|
|||||||
// 휴대폰번호 없는 사용자
|
// 휴대폰번호 없는 사용자
|
||||||
return LoginResponseDto.builder()
|
return LoginResponseDto.builder()
|
||||||
.success(false)
|
.success(false)
|
||||||
.errorMessage("휴대폰번호가 등록되어 있지 않아 로그인할 수 없습니다. 관리자에게 문의하세요.")
|
.errorMessage("NO_PHONE_NUMBER")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +234,7 @@ public class MainController implements InterceptorSkipController {
|
|||||||
if (!sent) {
|
if (!sent) {
|
||||||
return LoginResponseDto.builder()
|
return LoginResponseDto.builder()
|
||||||
.success(false)
|
.success(false)
|
||||||
.errorMessage("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.")
|
.errorMessage("FAIL_SEND_AUTH")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +467,29 @@ public class MainController implements InterceptorSkipController {
|
|||||||
MonitoringContext.RMS_PASSWORD_HISTORY_COUNT, DEFAULT_PASSWORD_HISTORY_COUNT);
|
MonitoringContext.RMS_PASSWORD_HISTORY_COUNT, DEFAULT_PASSWORD_HISTORY_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 비밀번호 변경 시 SMS 2차 인증 사용 여부 (djb.sms_auth.enabled 와 rms.password.sms_auth.enabled 가 모두 true 여야 동작)
|
||||||
|
private boolean isPasswordChangeSmsAuthEnabled() {
|
||||||
|
return monitoringContext.getBooleanProperty(MonitoringContext.RMS_PASSWORD_SMS_AUTH_ENABLED, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비밀번호 최소 길이 조회 (rms.password.length.check)
|
||||||
|
private int getPasswordLengthCheck() {
|
||||||
|
return monitoringContext.getIntProperty(
|
||||||
|
MonitoringContext.RMS_PASSWORD_LENGTH_CHECK, DEFAULT_PASSWORD_LENGTH_CHECK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비밀번호 문자 조합 종류 수 조회 (rms.password.combi.check, 영문/숫자/특수문자 중 N종류 이상)
|
||||||
|
private int getPasswordCombiCheck() {
|
||||||
|
return monitoringContext.getIntProperty(
|
||||||
|
MonitoringContext.RMS_PASSWORD_COMBI_CHECK, DEFAULT_PASSWORD_COMBI_CHECK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 동일 문자 연속 반복 제한 개수 조회 (rms.password.repeat.check, 2 미만이면 미검사)
|
||||||
|
private int getPasswordRepeatCheck() {
|
||||||
|
return monitoringContext.getIntProperty(
|
||||||
|
MonitoringContext.RMS_PASSWORD_REPEAT_CHECK, DEFAULT_PASSWORD_REPEAT_CHECK);
|
||||||
|
}
|
||||||
|
|
||||||
// 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금
|
// 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금
|
||||||
private int increaseLoginFailCount(UserInfo userInfo) {
|
private int increaseLoginFailCount(UserInfo userInfo) {
|
||||||
int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1;
|
int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1;
|
||||||
@@ -909,8 +935,9 @@ public class MainController implements InterceptorSkipController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. SMS 2차 인증 필요 여부 체크 (비상모드는 로그인과 동일하게 우회)
|
// 5. SMS 2차 인증 필요 여부 체크 (비상모드는 로그인과 동일하게 우회)
|
||||||
|
// djb.sms_auth.enabled 와 rms.password.sms_auth.enabled 가 모두 true 일 때만 SMS 인증 수행
|
||||||
Boolean emergencyMode = (Boolean) session.getAttribute("emergencyMode");
|
Boolean emergencyMode = (Boolean) session.getAttribute("emergencyMode");
|
||||||
if (!Boolean.TRUE.equals(emergencyMode) && smsAuthService.isEnabled()) {
|
if (!Boolean.TRUE.equals(emergencyMode) && smsAuthService.isEnabled() && isPasswordChangeSmsAuthEnabled()) {
|
||||||
if (!smsAuthService.hasValidPhoneNumber(userInfo)) {
|
if (!smsAuthService.hasValidPhoneNumber(userInfo)) {
|
||||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", "휴대폰번호 미등록으로 비밀번호 변경 불가");
|
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", "휴대폰번호 미등록으로 비밀번호 변경 불가");
|
||||||
return ChangePasswordResponseDto.builder()
|
return ChangePasswordResponseDto.builder()
|
||||||
@@ -1012,10 +1039,17 @@ public class MainController implements InterceptorSkipController {
|
|||||||
|
|
||||||
private boolean isPasswordValid(String password) {
|
private boolean isPasswordValid(String password) {
|
||||||
|
|
||||||
return password != null && password.getBytes().length == password
|
if (password == null || password.getBytes().length != password.length()) {
|
||||||
.length() && password.getBytes().length >= 7
|
return false;
|
||||||
&& isCombination(password)
|
}
|
||||||
&& !isRepeatPassword(password, PASSWORD_PATTERN_CHECK_LENGTH)
|
|
||||||
|
int lengthCheck = getPasswordLengthCheck();
|
||||||
|
int combiCheck = getPasswordCombiCheck();
|
||||||
|
int repeatCheck = getPasswordRepeatCheck();
|
||||||
|
|
||||||
|
return password.getBytes().length >= lengthCheck
|
||||||
|
&& isCombination(password, combiCheck)
|
||||||
|
&& (repeatCheck < 2 || !isRepeatPassword(password, repeatCheck))
|
||||||
&& !isSerialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH)
|
&& !isSerialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH)
|
||||||
&& !isKeyboardSequentialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH);
|
&& !isKeyboardSequentialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH);
|
||||||
}
|
}
|
||||||
@@ -1082,7 +1116,7 @@ public class MainController implements InterceptorSkipController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCombination(String password) {
|
private boolean isCombination(String password, int minTypes) {
|
||||||
int combicount = 0;
|
int combicount = 0;
|
||||||
// 숫자포함여부
|
// 숫자포함여부
|
||||||
String pattern = "[0-9]+";
|
String pattern = "[0-9]+";
|
||||||
@@ -1111,8 +1145,8 @@ public class MainController implements InterceptorSkipController {
|
|||||||
combicount++;
|
combicount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// combicount가 2 이상이면 true, 그렇지 않으면 false 반환
|
// combicount가 minTypes 이상이면 true, 그렇지 않으면 false 반환
|
||||||
return combicount >= 2;
|
return combicount >= minTypes;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
|
|||||||
import com.eactive.eai.data.jpa.AbstractDataService;
|
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||||
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUISearch;
|
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUISearch;
|
||||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||||
|
import com.querydsl.core.Tuple;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -47,6 +50,24 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 검색 콤보용 그룹 ID/명칭. 게시 여부와 관계없이 모든 그룹을 제공한다. */
|
||||||
|
public List<Map<String, String>> findGroupOptions() {
|
||||||
|
QApiGroup group = QApiGroup.apiGroup;
|
||||||
|
List<Tuple> rows = getJPAQueryFactory()
|
||||||
|
.select(group.id, group.groupName)
|
||||||
|
.from(group)
|
||||||
|
.orderBy(group.displayOrder.asc().nullsLast(), group.groupName.asc(), group.id.asc())
|
||||||
|
.fetch();
|
||||||
|
List<Map<String, String>> options = new ArrayList<>();
|
||||||
|
for (Tuple row : rows) {
|
||||||
|
Map<String, String> option = new HashMap<>();
|
||||||
|
option.put("id", row.get(group.id));
|
||||||
|
option.put("groupName", StringUtils.defaultIfBlank(row.get(group.groupName), "그룹명 없음"));
|
||||||
|
options.add(option);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteApiGroupApiByApiId(String apiId) {
|
public void deleteApiGroupApiByApiId(String apiId) {
|
||||||
List<ApiGroup> apiGroups = repository.findAll();
|
List<ApiGroup> apiGroups = repository.findAll();
|
||||||
for (ApiGroup apiGroup : apiGroups) {
|
for (ApiGroup apiGroup : apiGroups) {
|
||||||
@@ -77,6 +98,28 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
|
|||||||
.fetch();
|
.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 목록 툴팁용 게시 그룹명. Oracle IN 절 제한에 맞춰 페이지의 API들을 일괄 조회한다. */
|
||||||
|
public Map<String, List<String>> findPublishedGroupNamesByApiIds(List<String> apiIds) {
|
||||||
|
Map<String, List<String>> groupNames = new HashMap<>();
|
||||||
|
QApiGroup group = QApiGroup.apiGroup;
|
||||||
|
QApiGroupApi membership = QApiGroupApi.apiGroupApi;
|
||||||
|
for (int from = 0; from < apiIds.size(); from += 1000) {
|
||||||
|
List<String> chunk = apiIds.subList(from, Math.min(from + 1000, apiIds.size()));
|
||||||
|
List<Tuple> rows = getJPAQueryFactory()
|
||||||
|
.select(membership.id.apiId, group.groupName)
|
||||||
|
.from(membership)
|
||||||
|
.join(membership.apiGroup, group)
|
||||||
|
.where(membership.id.apiId.in(chunk), group.displayYn.eq("1"))
|
||||||
|
.orderBy(group.displayOrder.asc().nullsLast(), group.groupName.asc(), group.id.asc())
|
||||||
|
.fetch();
|
||||||
|
for (Tuple row : rows) {
|
||||||
|
groupNames.computeIfAbsent(row.get(membership.id.apiId), key -> new ArrayList<>())
|
||||||
|
.add(StringUtils.defaultIfBlank(row.get(group.groupName), "그룹명 없음"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groupNames;
|
||||||
|
}
|
||||||
|
|
||||||
/** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */
|
/** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */
|
||||||
public void setApiGroupsForApi(String apiId, List<String> groupIds) {
|
public void setApiGroupsForApi(String apiId, List<String> groupIds) {
|
||||||
deleteApiGroupApiByApiId(apiId);
|
deleteApiGroupApiByApiId(apiId);
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package com.eactive.eai.rms.data.entity.onl.apim.apispec;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
|
||||||
|
import com.eactive.eai.rms.data.EMSDataSource;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.Repository;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
/** EAI 목록과 스키마가 다른 EMS 스펙의 ID/게시 여부만 조회한다. */
|
||||||
|
@EMSDataSource
|
||||||
|
public interface ApiSpecStatusRepository extends Repository<ApiSpecInfo, String> {
|
||||||
|
|
||||||
|
@Query("SELECT a.apiId FROM ApiSpecInfo a WHERE "
|
||||||
|
+ "(:status = 'PUBLISHED' AND a.displayYn = 'Y') OR "
|
||||||
|
+ "(:status = 'REGISTERED' AND (a.displayYn IS NULL OR a.displayYn <> 'Y'))")
|
||||||
|
List<String> findApiIdsByStatus(@Param("status") String status);
|
||||||
|
|
||||||
|
@Query("SELECT a.apiId AS apiId, a.displayYn AS displayYn FROM ApiSpecInfo a WHERE a.apiId IN :apiIds")
|
||||||
|
List<SpecStatus> findStatusesByApiIds(@Param("apiIds") Collection<String> apiIds);
|
||||||
|
|
||||||
|
interface SpecStatus {
|
||||||
|
String getApiId();
|
||||||
|
String getDisplayYn();
|
||||||
|
}
|
||||||
|
}
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
|
|
||||||
|
|
||||||
public interface InflowTokenInsufficient {
|
|
||||||
String getEaisvcname();
|
|
||||||
String getEaisvcdesc();
|
|
||||||
Long getCnt();
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.eactive.eai.rms.data.entity.onl.eaimsg;
|
package com.eactive.eai.rms.data.entity.onl.eaimsg;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
|
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.apim.apispec.ApiSpecStatusRepository;
|
||||||
import com.eactive.eai.data.entity.onl.adapter.QAdapter;
|
import com.eactive.eai.data.entity.onl.adapter.QAdapter;
|
||||||
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup;
|
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup;
|
||||||
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
|
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
|
||||||
@@ -51,6 +53,7 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
|||||||
|
|
||||||
/** EMSAPP(PTL_API_SPEC_INFO) 조회용. 다른 스키마라 EAI 메시지 쿼리에 조인할 수 없다 */
|
/** EMSAPP(PTL_API_SPEC_INFO) 조회용. 다른 스키마라 EAI 메시지 쿼리에 조인할 수 없다 */
|
||||||
private final ApiSpecInfoRepository apiSpecInfoRepository;
|
private final ApiSpecInfoRepository apiSpecInfoRepository;
|
||||||
|
private final ApiSpecStatusRepository apiSpecStatusRepository;
|
||||||
|
|
||||||
public Page<Tuple> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable, String sortname, String sortorder) {
|
public Page<Tuple> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable, String sortname, String sortorder) {
|
||||||
|
|
||||||
@@ -201,6 +204,32 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
|||||||
predicate
|
predicate
|
||||||
.and(qStdInfo.apifullpath.contains(eaiMessageUISearch.getSearchApiFullPath()));
|
.and(qStdInfo.apifullpath.contains(eaiMessageUISearch.getSearchApiFullPath()));
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchInOutType())) {
|
||||||
|
predicate.and(qeaiMessageEntity.eaisvcname.endsWith(eaiMessageUISearch.getSearchInOutType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
String specStatus = eaiMessageUISearch.getSearchSpecStatus();
|
||||||
|
if ("UNREGISTERED".equals(specStatus)) {
|
||||||
|
predicate.and(apiSpecRegisteredExpression(qeaiMessageEntity.eaisvcname).not());
|
||||||
|
} else if ("REGISTERED".equals(specStatus) || "PUBLISHED".equals(specStatus)) {
|
||||||
|
predicate.and(apiIdInExpression(qeaiMessageEntity.eaisvcname,
|
||||||
|
apiSpecStatusRepository.findApiIdsByStatus(specStatus)));
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiGroupId = eaiMessageUISearch.getSearchApiGroupId();
|
||||||
|
if (StringUtils.isNotBlank(apiGroupId)) {
|
||||||
|
QApiGroupApi membership = QApiGroupApi.apiGroupApi;
|
||||||
|
QApiGroup group = QApiGroup.apiGroup;
|
||||||
|
JPQLQuery<Integer> groupQuery = JPAExpressions.selectOne()
|
||||||
|
.from(membership)
|
||||||
|
.join(membership.apiGroup, group)
|
||||||
|
.where(membership.id.apiId.eq(qeaiMessageEntity.eaisvcname));
|
||||||
|
if ("UNREGISTERED".equals(apiGroupId)) {
|
||||||
|
predicate.and(groupQuery.notExists());
|
||||||
|
} else {
|
||||||
|
predicate.and(groupQuery.where(group.id.eq(apiGroupId)).exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchStatusCode())) {
|
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchStatusCode())) {
|
||||||
String statusCode = eaiMessageUISearch.getSearchStatusCode();
|
String statusCode = eaiMessageUISearch.getSearchStatusCode();
|
||||||
@@ -230,7 +259,10 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
|||||||
* API ID 를 EMS 커넥션에서 따로 읽어 IN 절로 넘긴다.</p>
|
* API ID 를 EMS 커넥션에서 따로 읽어 IN 절로 넘긴다.</p>
|
||||||
*/
|
*/
|
||||||
private BooleanExpression apiSpecRegisteredExpression(StringPath eaiSvcNamePath) {
|
private BooleanExpression apiSpecRegisteredExpression(StringPath eaiSvcNamePath) {
|
||||||
List<String> specApiIds = apiSpecInfoRepository.findAllApiIds();
|
return apiIdInExpression(eaiSvcNamePath, apiSpecInfoRepository.findAllApiIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
private BooleanExpression apiIdInExpression(StringPath eaiSvcNamePath, List<String> specApiIds) {
|
||||||
if (specApiIds.isEmpty()) {
|
if (specApiIds.isEmpty()) {
|
||||||
// in(빈 리스트) 는 JPQL 이 깨지므로 항상 거짓인 조건으로 대체한다
|
// in(빈 리스트) 는 JPQL 이 깨지므로 항상 거짓인 조건으로 대체한다
|
||||||
return Expressions.ONE.eq(Expressions.TWO);
|
return Expressions.ONE.eq(Expressions.TWO);
|
||||||
@@ -245,6 +277,18 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 현재 페이지의 스펙 상태를 일괄 조회한다. Oracle IN 절은 1,000개씩 나눈다. */
|
||||||
|
public Map<String, String> findApiSpecStatuses(List<String> apiIds) {
|
||||||
|
Map<String, String> statuses = new HashMap<>();
|
||||||
|
for (int from = 0; from < apiIds.size(); from += ORACLE_IN_LIMIT) {
|
||||||
|
int to = Math.min(from + ORACLE_IN_LIMIT, apiIds.size());
|
||||||
|
for (ApiSpecStatusRepository.SpecStatus spec : apiSpecStatusRepository.findStatusesByApiIds(apiIds.subList(from, to))) {
|
||||||
|
statuses.put(spec.getApiId(), "Y".equals(spec.getDisplayYn()) ? "PUBLISHED" : "REGISTERED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return statuses;
|
||||||
|
}
|
||||||
|
|
||||||
/** 노출 중인(display_yn='1') API 그룹에 편성된 API ID. API_GROUP 은 EAI 메시지와 같은 스키마다 */
|
/** 노출 중인(display_yn='1') API 그룹에 편성된 API ID. API_GROUP 은 EAI 메시지와 같은 스키마다 */
|
||||||
private JPQLQuery<String> displayedGroupApiIdSubQuery() {
|
private JPQLQuery<String> displayedGroupApiIdSubQuery() {
|
||||||
QApiGroupApi qApiGroupApi = QApiGroupApi.apiGroupApi;
|
QApiGroupApi qApiGroupApi = QApiGroupApi.apiGroupApi;
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ public class EAIMessageUISearch {
|
|||||||
private String searchRefKey;
|
private String searchRefKey;
|
||||||
|
|
||||||
private String searchApiFullPath;
|
private String searchApiFullPath;
|
||||||
|
/** API ID 마지막 자리 (1: 당발, 2: 타발) */
|
||||||
|
private String searchInOutType;
|
||||||
|
/** UNREGISTERED: 미등록, REGISTERED: 등록(비게시), PUBLISHED: 게시 */
|
||||||
|
private String searchSpecStatus;
|
||||||
|
/** 그룹 ID. UNREGISTERED이면 소속 그룹이 없는 API */
|
||||||
|
private String searchApiGroupId;
|
||||||
|
|
||||||
/** API_STATUS.STATUS_CODE (E:장애, D:지연, C:점검). "N" 이면 정상(코드 없음)만 조회 */
|
/** API_STATUS.STATUS_CODE (E:장애, D:지연, C:점검). "N" 이면 정상(코드 없음)만 조회 */
|
||||||
private String searchStatusCode;
|
private String searchStatusCode;
|
||||||
|
|||||||
+2
@@ -62,6 +62,8 @@ public class InboundErrorInfoService extends AbstractDataService<InboundErrorInf
|
|||||||
List<InboundErrorInfo> list = query
|
List<InboundErrorInfo> list = query
|
||||||
.select(qInboundErrorInfo)
|
.select(qInboundErrorInfo)
|
||||||
.orderBy(qInboundErrorInfo.erroccurhms.desc())
|
.orderBy(qInboundErrorInfo.erroccurhms.desc())
|
||||||
|
.offset(pageable.getOffset())
|
||||||
|
.limit(pageable.getPageSize())
|
||||||
.fetch();
|
.fetch();
|
||||||
|
|
||||||
return new PageImpl<>(list, pageable, totalCount);
|
return new PageImpl<>(list, pageable, totalCount);
|
||||||
|
|||||||
+3
-3
@@ -336,12 +336,12 @@ public class ApiStatsMinuteService
|
|||||||
|
|
||||||
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
|
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
|
||||||
endDateTime = LocalDateTime.parse(search.getSearchEndDateTime(), DATE_TIME_FORMATTER);
|
endDateTime = LocalDateTime.parse(search.getSearchEndDateTime(), DATE_TIME_FORMATTER);
|
||||||
// 1시간(60분) 초과 시 시작시간 기준 1시간 후로 강제 설정
|
// 1시간(60분) 초과 시 시작시간 기준 1시간치(정시~59분, statTime 조건이 양끝 포함이므로 -1분)로 강제 설정
|
||||||
if (java.time.Duration.between(startDateTime, endDateTime).toMinutes() > MAX_MINUTES) {
|
if (java.time.Duration.between(startDateTime, endDateTime).toMinutes() > MAX_MINUTES) {
|
||||||
endDateTime = startDateTime.plusMinutes(MAX_MINUTES);
|
endDateTime = startDateTime.plusMinutes(MAX_MINUTES - 1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
endDateTime = startDateTime.plusMinutes(MAX_MINUTES);
|
endDateTime = startDateTime.plusMinutes(MAX_MINUTES - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
search.setParsedStartDateTime(startDateTime);
|
search.setParsedStartDateTime(startDateTime);
|
||||||
|
|||||||
+6
@@ -31,6 +31,12 @@ public class LayoutSyncHistoryService extends AbstractDataService<LayoutSyncHist
|
|||||||
if (StringUtils.isNotBlank(search.getSearchLoutName())) {
|
if (StringUtils.isNotBlank(search.getSearchLoutName())) {
|
||||||
query.where(q.loutname.containsIgnoreCase(search.getSearchLoutName()));
|
query.where(q.loutname.containsIgnoreCase(search.getSearchLoutName()));
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchServiceName())) {
|
||||||
|
query.where(q.servicename.containsIgnoreCase(search.getSearchServiceName()));
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchCommand())) {
|
||||||
|
query.where(q.command.containsIgnoreCase(search.getSearchCommand()));
|
||||||
|
}
|
||||||
if (StringUtils.isNotBlank(search.getSearchStartDate())) {
|
if (StringUtils.isNotBlank(search.getSearchStartDate())) {
|
||||||
query.where(q.recvamndhms.goe(parseYYYYMMDD(search.getSearchStartDate()).atStartOfDay()));
|
query.where(q.recvamndhms.goe(parseYYYYMMDD(search.getSearchStartDate()).atStartOfDay()));
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -20,4 +20,12 @@ public class TransactionTrackingFilterService extends
|
|||||||
.fetch();
|
.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deleteAllByUserId(String userId) {
|
||||||
|
repository.deleteAll(findAllByUserId(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveAll(List<TransactionTrackingFilter> filters) {
|
||||||
|
repository.saveAll(filters);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-36
@@ -130,10 +130,14 @@ public class ApiStatusDetectionService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PortalPropertyService portalPropertyService;
|
private PortalPropertyService portalPropertyService;
|
||||||
|
|
||||||
/** 자동 초안 문구 양식 (classpath:apistatus-draft.yml) */
|
/** 공지 문구 양식 (PTL_PROPERTY > classpath:apistatus-draft.yml) */
|
||||||
@Autowired
|
@Autowired
|
||||||
private ApiStatusDraftTemplate draftTemplate;
|
private ApiStatusDraftTemplate draftTemplate;
|
||||||
|
|
||||||
|
/** 제목·본문 조립. 관리자 수동 등록 화면과 같은 양식을 쓰기 위해 분리했다 */
|
||||||
|
@Autowired
|
||||||
|
private ApiStatusNoticeComposer noticeComposer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이벤트 감지
|
* 이벤트 감지
|
||||||
* @param event "CHECK_START" : 점검시작
|
* @param event "CHECK_START" : 점검시작
|
||||||
@@ -642,7 +646,7 @@ public class ApiStatusDetectionService {
|
|||||||
|
|
||||||
/** "GW 인터페이스 3건" - 게시되지 않은 인터페이스 묶음 표기 */
|
/** "GW 인터페이스 3건" - 게시되지 않은 인터페이스 묶음 표기 */
|
||||||
private String hiddenLabel(int hiddenCount) {
|
private String hiddenLabel(int hiddenCount) {
|
||||||
return draftTemplate.text(ApiStatusDraftTemplate.GW_LABEL, "count", String.valueOf(hiddenCount));
|
return noticeComposer.hiddenLabel(hiddenCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -667,16 +671,17 @@ public class ApiStatusDetectionService {
|
|||||||
*/
|
*/
|
||||||
private String buildTitle(String titleKeyword, List<String> apiIds, Map<String, String> apiNames) {
|
private String buildTitle(String titleKeyword, List<String> apiIds, Map<String, String> apiNames) {
|
||||||
ApiSplit split = splitPublished(apiIds);
|
ApiSplit split = splitPublished(apiIds);
|
||||||
if (split.published.isEmpty()) {
|
return noticeComposer.buildTitle(titleKeyword, labelsOf(split.published, apiNames),
|
||||||
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_HIDDEN_ONLY,
|
split.hiddenCount, apiIds.size());
|
||||||
"gw", hiddenLabel(apiIds.size()), "keyword", titleKeyword);
|
}
|
||||||
|
|
||||||
|
/** 인터페이스 ID 목록을 표시명 목록으로 바꾼다 (명칭을 못 찾으면 ID 그대로) */
|
||||||
|
private List<String> labelsOf(List<String> apiIds, Map<String, String> apiNames) {
|
||||||
|
List<String> labels = new ArrayList<>();
|
||||||
|
for (String apiId : apiIds) {
|
||||||
|
labels.add(apiLabel(apiId, apiNames));
|
||||||
}
|
}
|
||||||
String first = apiLabel(split.published.get(0), apiNames);
|
return labels;
|
||||||
if (apiIds.size() > 1) {
|
|
||||||
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MULTI,
|
|
||||||
"first", first, "rest", String.valueOf(apiIds.size() - 1), "keyword", titleKeyword);
|
|
||||||
}
|
|
||||||
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_SINGLE, "first", first, "keyword", titleKeyword);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -690,39 +695,19 @@ public class ApiStatusDetectionService {
|
|||||||
*/
|
*/
|
||||||
private PortalNotice createNotice(String title, String summary, List<String> apiIds,
|
private PortalNotice createNotice(String title, String summary, List<String> apiIds,
|
||||||
Map<String, String> apiNames, LocalDateTime now, boolean publish) {
|
Map<String, String> apiNames, LocalDateTime now, boolean publish) {
|
||||||
StringBuilder detail = new StringBuilder();
|
String lead = draftTemplate.text(publish
|
||||||
detail.append(draftTemplate.text(publish
|
|
||||||
? ApiStatusDraftTemplate.BODY_LEAD_PUBLISH
|
? ApiStatusDraftTemplate.BODY_LEAD_PUBLISH
|
||||||
: ApiStatusDraftTemplate.BODY_LEAD_DRAFT,
|
: ApiStatusDraftTemplate.BODY_LEAD_DRAFT,
|
||||||
"summary", summary));
|
"summary", summary);
|
||||||
|
|
||||||
// 게시된 API 만 이름으로 적고(인터페이스 ID 는 넣지 않는다) 나머지는 건수로 묶는다.
|
|
||||||
// 공지 본문은 저장 시점에 굳는 HTML 이라 나중에 사용자별로 가릴 수 없다.
|
|
||||||
ApiSplit split = splitPublished(apiIds);
|
ApiSplit split = splitPublished(apiIds);
|
||||||
detail.append("<p><b>")
|
String detail = noticeComposer.buildBody(
|
||||||
.append(draftTemplate.text(ApiStatusDraftTemplate.BODY_AFFECTED_HEADING))
|
lead, labelsOf(split.published, apiNames), split.hiddenCount, !publish);
|
||||||
.append("</b></p><ul>");
|
|
||||||
for (String apiId : split.published) {
|
|
||||||
detail.append("<li>").append(apiLabel(apiId, apiNames)).append("</li>");
|
|
||||||
}
|
|
||||||
if (split.hiddenCount > 0) {
|
|
||||||
detail.append("<li>").append(hiddenLabel(split.hiddenCount)).append("</li>");
|
|
||||||
}
|
|
||||||
detail.append("</ul>");
|
|
||||||
|
|
||||||
for (ApiStatusDraftTemplate.Section section : draftTemplate.getSections()) {
|
|
||||||
detail.append("<p><b>").append(section.getTitle()).append("</b></p>");
|
|
||||||
// 게시 상태로 나가는 본문에 "작성 필요" 가 그대로 보이면 안 되므로 문구를 나눈다
|
|
||||||
detail.append("<p>").append(publish
|
|
||||||
? draftTemplate.text(ApiStatusDraftTemplate.BODY_FILLED_TEXT)
|
|
||||||
: draftTemplate.text(ApiStatusDraftTemplate.BODY_DRAFT_TEXT, "hint", section.getHint()))
|
|
||||||
.append("</p>");
|
|
||||||
}
|
|
||||||
|
|
||||||
PortalNotice notice = new PortalNotice();
|
PortalNotice notice = new PortalNotice();
|
||||||
notice.setId(null);
|
notice.setId(null);
|
||||||
notice.setNoticeSubject(StringUtils.abbreviate(title, NOTICE_SUBJECT_MAX_LENGTH));
|
notice.setNoticeSubject(StringUtils.abbreviate(title, NOTICE_SUBJECT_MAX_LENGTH));
|
||||||
notice.setNoticeDetail(detail.toString());
|
notice.setNoticeDetail(detail);
|
||||||
notice.setNoticeType(NOTICE_TYPE_INCIDENT);
|
notice.setNoticeType(NOTICE_TYPE_INCIDENT);
|
||||||
notice.setUseYn(publish ? "Y" : "N");
|
notice.setUseYn(publish ? "Y" : "N");
|
||||||
notice.setFixYn("N");
|
notice.setFixYn("N");
|
||||||
|
|||||||
@@ -11,21 +11,32 @@ import javax.annotation.PostConstruct;
|
|||||||
|
|
||||||
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.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.yaml.snakeyaml.LoaderOptions;
|
import org.yaml.snakeyaml.LoaderOptions;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
import org.yaml.snakeyaml.Yaml;
|
||||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Status 자동 탐지 초안 양식({@code classpath:apistatus-draft.yml}).
|
* API Status 공지 양식. 자동 탐지 초안과 관리자 수동 등록이 같은 문구를 쓴다.
|
||||||
*
|
*
|
||||||
* <p>자동 생성 공지의 제목·본문·타임라인 문구를 코드가 아닌 설정으로 관리한다.
|
* <p>문구 출처는 우선순위 3단이다.</p>
|
||||||
* admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없으므로 SnakeYAML 로 직접 읽는다
|
* <ol>
|
||||||
* ({@code <context:property-placeholder>} 는 properties 전용이고, 검수 섹션이 리스트라
|
* <li>{@code PTL_PROPERTY} (그룹 {@link #PROPERTY_GROUP}, 키 {@link #PROPERTY_PREFIX} + 문구키)
|
||||||
* properties 로는 인덱스 키로 흩어진다).</p>
|
* - 운영 중 포탈 프로퍼티 관리 화면에서 재배포 없이 고칠 수 있다.</li>
|
||||||
|
* <li>{@code classpath:apistatus-draft.yml} - 배포본에 남기는 문구 원본.</li>
|
||||||
|
* <li>코드 {@link #DEFAULTS} - 위 둘이 모두 없을 때.</li>
|
||||||
|
* </ol>
|
||||||
*
|
*
|
||||||
* <p>파일이 없거나 파싱에 실패하면 {@link #DEFAULTS} 로 동작한다 - 문구 설정 문제로
|
* <p>yml 을 SnakeYAML 로 직접 읽는 이유: admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없고
|
||||||
* 장애 탐지 자체가 멈추면 안 되기 때문이다. 개별 키가 비어도 같은 이유로 기본값으로 떨어진다.</p>
|
* ({@code <context:property-placeholder>} 는 properties 전용), 검수 섹션이 리스트라 properties 로는
|
||||||
|
* 인덱스 키로 흩어진다.</p>
|
||||||
|
*
|
||||||
|
* <p>DB 조회는 60초 스냅샷으로 캐시한다 - 공지 한 건을 만드는 데 {@link #text} 가 수십 번 불리므로
|
||||||
|
* 매번 조회하면 안 된다. 조회에 실패하거나 개별 키가 비어 있으면 그 아래 단계 문구로 떨어진다.
|
||||||
|
* 문구 설정 문제로 장애 탐지 자체가 멈추면 안 되기 때문이다.</p>
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class ApiStatusDraftTemplate {
|
public class ApiStatusDraftTemplate {
|
||||||
@@ -34,6 +45,15 @@ public class ApiStatusDraftTemplate {
|
|||||||
|
|
||||||
private static final String RESOURCE = "apistatus-draft.yml";
|
private static final String RESOURCE = "apistatus-draft.yml";
|
||||||
|
|
||||||
|
/** 문구 프로퍼티 그룹 (개발자포탈과 동일 그룹을 쓴다) */
|
||||||
|
public static final String PROPERTY_GROUP = "Portal";
|
||||||
|
|
||||||
|
/** 문구 프로퍼티 키 접두어. 접두어 뒤는 아래 문구 키와 같다 */
|
||||||
|
public static final String PROPERTY_PREFIX = "djb.apistatus.draft.";
|
||||||
|
|
||||||
|
/** DB 스냅샷 유효 시간 (ms) */
|
||||||
|
private static final long REFRESH_INTERVAL_MS = 60_000L;
|
||||||
|
|
||||||
// ---- 키 ----
|
// ---- 키 ----
|
||||||
public static final String GW_LABEL = "gw-label";
|
public static final String GW_LABEL = "gw-label";
|
||||||
|
|
||||||
@@ -41,12 +61,21 @@ public class ApiStatusDraftTemplate {
|
|||||||
public static final String TITLE_MULTI = "title.multi";
|
public static final String TITLE_MULTI = "title.multi";
|
||||||
public static final String TITLE_HIDDEN_ONLY = "title.hidden-only";
|
public static final String TITLE_HIDDEN_ONLY = "title.hidden-only";
|
||||||
|
|
||||||
|
/** 관리자 수동 등록 제목 - 자동 탐지 표기([자동감지]·GW 인터페이스)를 쓰지 않는다 */
|
||||||
|
public static final String TITLE_MANUAL_SINGLE = "title.manual-single";
|
||||||
|
public static final String TITLE_MANUAL_MULTI = "title.manual-multi";
|
||||||
|
public static final String TITLE_MANUAL_EMPTY = "title.manual-empty";
|
||||||
|
|
||||||
public static final String BODY_LEAD_PUBLISH = "body.lead-publish";
|
public static final String BODY_LEAD_PUBLISH = "body.lead-publish";
|
||||||
public static final String BODY_LEAD_DRAFT = "body.lead-draft";
|
public static final String BODY_LEAD_DRAFT = "body.lead-draft";
|
||||||
|
public static final String BODY_LEAD_MANUAL = "body.lead-manual";
|
||||||
public static final String BODY_AFFECTED_HEADING = "body.affected-heading";
|
public static final String BODY_AFFECTED_HEADING = "body.affected-heading";
|
||||||
public static final String BODY_FILLED_TEXT = "body.filled-text";
|
public static final String BODY_FILLED_TEXT = "body.filled-text";
|
||||||
public static final String BODY_DRAFT_TEXT = "body.draft-text";
|
public static final String BODY_DRAFT_TEXT = "body.draft-text";
|
||||||
|
|
||||||
|
/** 검수 항목. 프로퍼티에서는 {@code 제목|힌트} 를 {@code ;;} 로 이어 붙인 문자열 한 건이다 */
|
||||||
|
public static final String BODY_SECTIONS = "body.sections";
|
||||||
|
|
||||||
public static final String TL_DETECTED = "timeline.detected";
|
public static final String TL_DETECTED = "timeline.detected";
|
||||||
public static final String TL_API_ADDED = "timeline.api-added";
|
public static final String TL_API_ADDED = "timeline.api-added";
|
||||||
public static final String TL_REDETECTED = "timeline.redetected";
|
public static final String TL_REDETECTED = "timeline.redetected";
|
||||||
@@ -54,17 +83,30 @@ public class ApiStatusDraftTemplate {
|
|||||||
public static final String TL_RECOVERED_ALL = "timeline.recovered-all";
|
public static final String TL_RECOVERED_ALL = "timeline.recovered-all";
|
||||||
public static final String TL_RECOVERED_PART = "timeline.recovered-part";
|
public static final String TL_RECOVERED_PART = "timeline.recovered-part";
|
||||||
|
|
||||||
|
/** 검수 항목 프로퍼티 구분자 - 항목 사이 */
|
||||||
|
private static final String SECTION_DELIMITER = ";;";
|
||||||
|
|
||||||
|
/** 검수 항목 프로퍼티 구분자 - 제목과 힌트 사이 */
|
||||||
|
private static final String SECTION_FIELD_DELIMITER = "|";
|
||||||
|
|
||||||
/** yml 을 못 읽었을 때 쓰는 기본 문구 (기존 하드코딩과 동일) */
|
/** yml 을 못 읽었을 때 쓰는 기본 문구 (기존 하드코딩과 동일) */
|
||||||
private static final Map<String, String> DEFAULTS;
|
private static final Map<String, String> DEFAULTS;
|
||||||
|
|
||||||
|
/** 프로퍼티 최초 등록 시 함께 적는 설명 (관리 화면에서 치환자 의미를 보여준다) */
|
||||||
|
private static final Map<String, String> DESCRIPTIONS;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
Map<String, String> defaults = new LinkedHashMap<>();
|
Map<String, String> defaults = new LinkedHashMap<>();
|
||||||
defaults.put(GW_LABEL, "GW 인터페이스 {count}건");
|
defaults.put(GW_LABEL, "GW 인터페이스 {count}건");
|
||||||
defaults.put(TITLE_SINGLE, "[자동감지] {first} API {keyword}");
|
defaults.put(TITLE_SINGLE, "[자동감지] {first} API {keyword}");
|
||||||
defaults.put(TITLE_MULTI, "[자동감지] {first} 외 {rest}종 API {keyword}");
|
defaults.put(TITLE_MULTI, "[자동감지] {first} 외 {rest}종 API {keyword}");
|
||||||
defaults.put(TITLE_HIDDEN_ONLY, "[자동감지] {gw} {keyword}");
|
defaults.put(TITLE_HIDDEN_ONLY, "[자동감지] {gw} {keyword}");
|
||||||
|
defaults.put(TITLE_MANUAL_SINGLE, "{first} API {keyword}");
|
||||||
|
defaults.put(TITLE_MANUAL_MULTI, "{first} 외 {rest}종 API {keyword}");
|
||||||
|
defaults.put(TITLE_MANUAL_EMPTY, "API {keyword}");
|
||||||
defaults.put(BODY_LEAD_PUBLISH, "<p>{summary} 로 자동 감지된 장애입니다. 상세 내용은 확인 후 갱신될 수 있습니다.</p>");
|
defaults.put(BODY_LEAD_PUBLISH, "<p>{summary} 로 자동 감지된 장애입니다. 상세 내용은 확인 후 갱신될 수 있습니다.</p>");
|
||||||
defaults.put(BODY_LEAD_DRAFT, "<p>{summary} 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.</p>");
|
defaults.put(BODY_LEAD_DRAFT, "<p>{summary} 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.</p>");
|
||||||
|
defaults.put(BODY_LEAD_MANUAL, "<p>[작성 필요] 장애 개요를 입력해 주십시오.</p>");
|
||||||
defaults.put(BODY_AFFECTED_HEADING, "영향 API");
|
defaults.put(BODY_AFFECTED_HEADING, "영향 API");
|
||||||
defaults.put(BODY_FILLED_TEXT, "확인 중입니다.");
|
defaults.put(BODY_FILLED_TEXT, "확인 중입니다.");
|
||||||
defaults.put(BODY_DRAFT_TEXT, "[작성 필요] {hint}");
|
defaults.put(BODY_DRAFT_TEXT, "[작성 필요] {hint}");
|
||||||
@@ -75,6 +117,29 @@ public class ApiStatusDraftTemplate {
|
|||||||
defaults.put(TL_RECOVERED_ALL, "전체 복구 확인 ({event})\n{affected}");
|
defaults.put(TL_RECOVERED_ALL, "전체 복구 확인 ({event})\n{affected}");
|
||||||
defaults.put(TL_RECOVERED_PART, "일부 복구 확인 ({event})\n{affected}");
|
defaults.put(TL_RECOVERED_PART, "일부 복구 확인 ({event})\n{affected}");
|
||||||
DEFAULTS = Collections.unmodifiableMap(defaults);
|
DEFAULTS = Collections.unmodifiableMap(defaults);
|
||||||
|
|
||||||
|
Map<String, String> descriptions = new LinkedHashMap<>();
|
||||||
|
descriptions.put(GW_LABEL, "개발자포탈 미게시 인터페이스 묶음 라벨. {count}=건수");
|
||||||
|
descriptions.put(TITLE_SINGLE, "공지 제목(영향 API 1건). {first}=API명, {keyword}=장애|응답지연");
|
||||||
|
descriptions.put(TITLE_MULTI, "공지 제목(영향 API 2건 이상). {first}=첫 API명, {rest}=나머지 건수, {keyword}=장애|응답지연");
|
||||||
|
descriptions.put(TITLE_HIDDEN_ONLY, "게시 API 가 하나도 없을 때 공지 제목. {gw}=gw-label 적용 결과, {keyword}=장애|응답지연");
|
||||||
|
descriptions.put(TITLE_MANUAL_SINGLE, "수동 등록 공지 제목(영향 API 1건). {first}=API명, {keyword}=장애");
|
||||||
|
descriptions.put(TITLE_MANUAL_MULTI, "수동 등록 공지 제목(영향 API 2건 이상). {first}=첫 API명, {rest}=나머지 건수, {keyword}=장애");
|
||||||
|
descriptions.put(TITLE_MANUAL_EMPTY, "수동 등록 공지 제목(영향 API 미선택). {keyword}=장애");
|
||||||
|
descriptions.put(BODY_LEAD_PUBLISH, "본문 머리말(자동 탐지 즉시 게시). {summary}=탐지 사유");
|
||||||
|
descriptions.put(BODY_LEAD_DRAFT, "본문 머리말(자동 탐지 초안). {summary}=탐지 사유");
|
||||||
|
descriptions.put(BODY_LEAD_MANUAL, "본문 머리말(관리자 수동 등록). 치환 변수 없음");
|
||||||
|
descriptions.put(BODY_AFFECTED_HEADING, "영향 API 목록 소제목. 치환 변수 없음");
|
||||||
|
descriptions.put(BODY_FILLED_TEXT, "게시 상태로 나가는 검수 항목 기본 문구. 치환 변수 없음");
|
||||||
|
descriptions.put(BODY_DRAFT_TEXT, "초안 상태 검수 항목 문구. {hint}=body.sections 의 힌트");
|
||||||
|
descriptions.put(BODY_SECTIONS, "관리자가 채울 검수 항목. '제목|힌트' 를 ';;' 로 구분. 전체 1000바이트(한글 약 330자) 이내");
|
||||||
|
descriptions.put(TL_DETECTED, "최초 탐지 타임라인. {summary}=탐지 사유, {event}=ERROR_START 등, {affected}=영향 API 요약");
|
||||||
|
descriptions.put(TL_API_ADDED, "영향 API 추가 타임라인. {event}=이벤트, {affected}=영향 API 요약");
|
||||||
|
descriptions.put(TL_REDETECTED, "재탐지 타임라인. {event}=이벤트");
|
||||||
|
descriptions.put(TL_ESCALATED, "지연에서 장애로 격상 타임라인. {event}=이벤트");
|
||||||
|
descriptions.put(TL_RECOVERED_ALL, "전체 복구 타임라인. {event}=이벤트, {affected}=영향 API 요약");
|
||||||
|
descriptions.put(TL_RECOVERED_PART, "일부 복구 타임라인. {event}=이벤트, {affected}=영향 API 요약");
|
||||||
|
DESCRIPTIONS = Collections.unmodifiableMap(descriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 관리자가 검수 시 채워 넣을 항목 */
|
/** 관리자가 검수 시 채워 넣을 항목 */
|
||||||
@@ -106,10 +171,24 @@ public class ApiStatusDraftTemplate {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 점(.) 으로 평탄화한 문구 맵 */
|
@Autowired(required = false)
|
||||||
private Map<String, String> texts = DEFAULTS;
|
private PortalPropertyService portalPropertyService;
|
||||||
private List<Section> sections = DEFAULT_SECTIONS;
|
|
||||||
|
|
||||||
|
/** yml/코드 기본값 - DB 프로퍼티가 없을 때 쓰는 바탕 문구 */
|
||||||
|
private Map<String, String> baselineTexts = DEFAULTS;
|
||||||
|
private List<Section> baselineSections = DEFAULT_SECTIONS;
|
||||||
|
|
||||||
|
/** 바탕 문구 위에 DB 프로퍼티를 덮은 결과 (실제 사용분) */
|
||||||
|
private volatile Map<String, String> texts = DEFAULTS;
|
||||||
|
private volatile List<Section> sections = DEFAULT_SECTIONS;
|
||||||
|
|
||||||
|
/** 다음 DB 스냅샷 갱신 시각 (ms). 0 이면 아직 한 번도 안 읽음 */
|
||||||
|
private volatile long nextRefreshAt = 0L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* yml 바탕 문구 로딩. DB 는 여기서 읽지 않는다 - 기동 시점에는 데이터소스·테넌트가
|
||||||
|
* 준비되지 않았을 수 있고, 프로퍼티 조회 실패로 컨텍스트 기동이 막히면 안 된다.
|
||||||
|
*/
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void load() {
|
public void load() {
|
||||||
try (InputStream in = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
try (InputStream in = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||||
@@ -127,14 +206,17 @@ public class ApiStatusDraftTemplate {
|
|||||||
|
|
||||||
Map<String, String> loaded = new LinkedHashMap<>(DEFAULTS);
|
Map<String, String> loaded = new LinkedHashMap<>(DEFAULTS);
|
||||||
flatten("", draft, loaded);
|
flatten("", draft, loaded);
|
||||||
this.texts = loaded;
|
this.baselineTexts = loaded;
|
||||||
this.sections = readSections(draft);
|
this.baselineSections = readSections(draft);
|
||||||
log.info("초안 양식 로딩 완료: {} (섹션 {}개)", RESOURCE, sections.size());
|
log.info("초안 양식 로딩 완료: {} (섹션 {}개)", RESOURCE, baselineSections.size());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 문구 설정 문제로 장애 탐지가 멈추면 안 된다
|
// 문구 설정 문제로 장애 탐지가 멈추면 안 된다
|
||||||
log.warn("{} 로딩 실패 - 기본 초안 양식 사용", RESOURCE, e);
|
log.warn("{} 로딩 실패 - 기본 초안 양식 사용", RESOURCE, e);
|
||||||
this.texts = DEFAULTS;
|
this.baselineTexts = DEFAULTS;
|
||||||
this.sections = DEFAULT_SECTIONS;
|
this.baselineSections = DEFAULT_SECTIONS;
|
||||||
|
} finally {
|
||||||
|
this.texts = this.baselineTexts;
|
||||||
|
this.sections = this.baselineSections;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +225,11 @@ public class ApiStatusDraftTemplate {
|
|||||||
* 정의되지 않은 치환자는 그대로 남겨 어떤 키가 빠졌는지 화면에서 드러나게 한다.
|
* 정의되지 않은 치환자는 그대로 남겨 어떤 키가 빠졌는지 화면에서 드러나게 한다.
|
||||||
*/
|
*/
|
||||||
public String text(String key, String... args) {
|
public String text(String key, String... args) {
|
||||||
|
ensureFresh();
|
||||||
String template = texts.get(key);
|
String template = texts.get(key);
|
||||||
|
if (template == null) {
|
||||||
|
template = baselineTexts.get(key);
|
||||||
|
}
|
||||||
if (template == null) {
|
if (template == null) {
|
||||||
template = DEFAULTS.get(key);
|
template = DEFAULTS.get(key);
|
||||||
}
|
}
|
||||||
@@ -159,9 +245,124 @@ public class ApiStatusDraftTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<Section> getSections() {
|
public List<Section> getSections() {
|
||||||
|
ensureFresh();
|
||||||
return sections;
|
return sections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────── PTL_PROPERTY 스냅샷 ──────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB 스냅샷이 만료됐으면 그룹 전체를 한 번에 다시 읽어 바탕 문구 위에 덮는다.
|
||||||
|
* 조회에 실패해도 다음 주기까지는 재시도하지 않는다 - DB 가 아플 때 공지 한 건마다 매달리면 안 된다.
|
||||||
|
*/
|
||||||
|
private void ensureFresh() {
|
||||||
|
if (portalPropertyService == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (System.currentTimeMillis() < nextRefreshAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
if (System.currentTimeMillis() < nextRefreshAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
refreshFromProperties();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("공지 양식 프로퍼티 조회 실패 - 파일/기본 문구 사용", e);
|
||||||
|
} finally {
|
||||||
|
nextRefreshAt = System.currentTimeMillis() + REFRESH_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshFromProperties() {
|
||||||
|
Map<String, String> group = portalPropertyService.getPortalPropertiesAsMap(PROPERTY_GROUP);
|
||||||
|
|
||||||
|
Map<String, String> merged = new LinkedHashMap<>(baselineTexts);
|
||||||
|
String sectionsValue = null;
|
||||||
|
for (String key : DESCRIPTIONS.keySet()) {
|
||||||
|
String value = group.get(PROPERTY_PREFIX + key);
|
||||||
|
if (isBlank(value)) {
|
||||||
|
// 프로퍼티가 없으면 지금 값으로 만들어 둔다 (Oracle 은 빈 문자열도 NULL 이라 같은 취급).
|
||||||
|
// 관리자가 값을 지운 경우엔 이미 행이 있으므로 새로 만들지 않고 바탕 문구로 떨어진다.
|
||||||
|
value = seedProperty(key);
|
||||||
|
}
|
||||||
|
if (isBlank(value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (BODY_SECTIONS.equals(key)) {
|
||||||
|
sectionsValue = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
merged.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Section> parsedSections = parseSections(sectionsValue);
|
||||||
|
|
||||||
|
this.texts = merged;
|
||||||
|
this.sections = parsedSections == null ? baselineSections : parsedSections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 프로퍼티가 없을 때 현재 바탕 문구를 기본값으로 등록하고 그 값을 돌려준다 */
|
||||||
|
private String seedProperty(String key) {
|
||||||
|
String defaultValue = BODY_SECTIONS.equals(key)
|
||||||
|
? formatSections(baselineSections)
|
||||||
|
: baselineTexts.get(key);
|
||||||
|
if (isBlank(defaultValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return portalPropertyService.getOrCreateProperty(
|
||||||
|
PROPERTY_GROUP, PROPERTY_PREFIX + key, defaultValue, DESCRIPTIONS.get(key));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("공지 양식 프로퍼티 등록 실패: {}{}", PROPERTY_PREFIX, key, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@code 제목|힌트;;제목|힌트} 파싱. 쓸 수 있는 항목이 없으면 null (호출부가 바탕 섹션을 쓴다) */
|
||||||
|
private List<Section> parseSections(String value) {
|
||||||
|
if (isBlank(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Section> result = new ArrayList<>();
|
||||||
|
for (String item : value.split(SECTION_DELIMITER)) {
|
||||||
|
String entry = item.trim();
|
||||||
|
if (entry.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int idx = entry.indexOf(SECTION_FIELD_DELIMITER);
|
||||||
|
String title = idx < 0 ? entry : entry.substring(0, idx).trim();
|
||||||
|
String hint = idx < 0 ? "" : entry.substring(idx + 1).trim();
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.add(new Section(title, hint));
|
||||||
|
}
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
log.warn("검수 항목 프로퍼티를 해석하지 못함 - 파일/기본 항목 사용: {}", value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 검수 항목을 프로퍼티 저장 형식으로 되돌린다 (최초 등록용) */
|
||||||
|
private String formatSections(List<Section> source) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Section section : source) {
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append(SECTION_DELIMITER);
|
||||||
|
}
|
||||||
|
sb.append(section.getTitle()).append(SECTION_FIELD_DELIMITER).append(section.getHint());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlank(String value) {
|
||||||
|
return value == null || value.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
// ────────────── 내부 헬퍼 ──────────────
|
// ────────────── 내부 헬퍼 ──────────────
|
||||||
|
|
||||||
/** 중첩 맵을 {@code body.lead-draft} 형태의 평탄 키로 편다. 리스트(sections)는 별도로 읽는다. */
|
/** 중첩 맵을 {@code body.lead-draft} 형태의 평탄 키로 편다. 리스트(sections)는 별도로 읽는다. */
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.apistatus;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Status 공지의 제목·본문 조립. 자동 탐지({@link ApiStatusDetectionService})와
|
||||||
|
* 관리자 수동 등록(공지사항 관리 화면)이 같은 양식을 쓰도록 문구 조립만 떼어 둔 것이다.
|
||||||
|
*
|
||||||
|
* <p>게시 여부 판정·API 명 조회는 하지 않는다 - 자동 탐지는 APIGW 스키마 조회가 필요하고
|
||||||
|
* 수동 등록은 영향 API 팝업이 이미 게시 API 만 고르게 되어 있어, 판정 책임을 호출부에 남긴다.
|
||||||
|
* 여기는 "게시 API 표시명 목록 + 미게시 건수" 만 받는다.</p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ApiStatusNoticeComposer {
|
||||||
|
|
||||||
|
private final ApiStatusDraftTemplate draftTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ApiStatusNoticeComposer(ApiStatusDraftTemplate draftTemplate) {
|
||||||
|
this.draftTemplate = draftTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공지 제목. 게시 API 가 하나도 없으면 인터페이스 명을 쓰지 않고 건수로만 적는다.
|
||||||
|
*
|
||||||
|
* @param keyword 제목 키워드 (장애 / 응답지연)
|
||||||
|
* @param publishedLabels 개발자포탈 게시 API 표시명 (노출 가능한 것만)
|
||||||
|
* @param hiddenCount 미게시 인터페이스 건수
|
||||||
|
* @param totalCount 영향 인터페이스 전체 건수
|
||||||
|
*/
|
||||||
|
public String buildTitle(String keyword, List<String> publishedLabels, int hiddenCount, int totalCount) {
|
||||||
|
if (publishedLabels == null || publishedLabels.isEmpty()) {
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_HIDDEN_ONLY,
|
||||||
|
"gw", hiddenLabel(totalCount), "keyword", keyword);
|
||||||
|
}
|
||||||
|
String first = publishedLabels.get(0);
|
||||||
|
if (totalCount > 1) {
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MULTI,
|
||||||
|
"first", first, "rest", String.valueOf(totalCount - 1), "keyword", keyword);
|
||||||
|
}
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_SINGLE, "first", first, "keyword", keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 수동 등록 공지 제목. 자동 탐지 표기("[자동감지]", "GW 인터페이스 N건")를 쓰지 않는다 -
|
||||||
|
* 사람이 직접 쓰는 공지이고, 영향 API 선택 팝업이 게시 API 만 보여주므로 미게시 묶음 표기도 필요 없다.
|
||||||
|
*
|
||||||
|
* @param keyword 제목 키워드 (장애)
|
||||||
|
* @param publishedLabels 화면에서 고른 영향 API 표시명. 비어 있으면 API 명 없는 제목을 만든다
|
||||||
|
*/
|
||||||
|
public String buildManualTitle(String keyword, List<String> publishedLabels) {
|
||||||
|
if (publishedLabels == null || publishedLabels.isEmpty()) {
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MANUAL_EMPTY, "keyword", keyword);
|
||||||
|
}
|
||||||
|
String first = publishedLabels.get(0);
|
||||||
|
if (publishedLabels.size() > 1) {
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MANUAL_MULTI,
|
||||||
|
"first", first, "rest", String.valueOf(publishedLabels.size() - 1), "keyword", keyword);
|
||||||
|
}
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MANUAL_SINGLE, "first", first, "keyword", keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공지 본문(HTML). 머리말 + 영향 API 목록 + 검수 항목 순이다.
|
||||||
|
*
|
||||||
|
* <p>게시된 API 만 이름으로 적고(인터페이스 ID 는 넣지 않는다) 나머지는 건수로 묶는다.
|
||||||
|
* 공지 본문은 저장 시점에 굳는 HTML 이라 나중에 사용자별로 가릴 수 없다.</p>
|
||||||
|
*
|
||||||
|
* @param leadText 머리말 (이미 치환이 끝난 문구)
|
||||||
|
* @param draftHints true 면 검수 항목에 작성 힌트를, false 면 게시용 기본 문구를 채운다
|
||||||
|
*/
|
||||||
|
public String buildBody(String leadText, List<String> publishedLabels, int hiddenCount, boolean draftHints) {
|
||||||
|
StringBuilder detail = new StringBuilder();
|
||||||
|
detail.append(leadText);
|
||||||
|
|
||||||
|
detail.append("<p><b>")
|
||||||
|
.append(draftTemplate.text(ApiStatusDraftTemplate.BODY_AFFECTED_HEADING))
|
||||||
|
.append("</b></p><ul>");
|
||||||
|
if (publishedLabels != null) {
|
||||||
|
for (String label : publishedLabels) {
|
||||||
|
detail.append("<li>").append(label).append("</li>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hiddenCount > 0) {
|
||||||
|
detail.append("<li>").append(hiddenLabel(hiddenCount)).append("</li>");
|
||||||
|
}
|
||||||
|
detail.append("</ul>");
|
||||||
|
|
||||||
|
for (ApiStatusDraftTemplate.Section section : draftTemplate.getSections()) {
|
||||||
|
detail.append("<p><b>").append(section.getTitle()).append("</b></p>");
|
||||||
|
// 게시 상태로 나가는 본문에 "작성 필요" 가 그대로 보이면 안 되므로 문구를 나눈다
|
||||||
|
detail.append("<p>").append(draftHints
|
||||||
|
? draftTemplate.text(ApiStatusDraftTemplate.BODY_DRAFT_TEXT, "hint", section.getHint())
|
||||||
|
: draftTemplate.text(ApiStatusDraftTemplate.BODY_FILLED_TEXT))
|
||||||
|
.append("</p>");
|
||||||
|
}
|
||||||
|
return detail.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "GW 인터페이스 3건" - 게시되지 않은 인터페이스 묶음 표기 */
|
||||||
|
public String hiddenLabel(int hiddenCount) {
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.GW_LABEL, "count", String.valueOf(hiddenCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,16 @@
|
|||||||
package com.eactive.eai.rms.ext.djb.inflow;
|
package com.eactive.eai.rms.ext.djb.inflow;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.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.InflowTokenInsufficientLog;
|
||||||
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLogId;
|
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> {
|
||||||
|
|
||||||
@Query(nativeQuery = true, value =
|
@Query(nativeQuery = true, value =
|
||||||
" SELECT EAISVCNAME" +
|
" SELECT COUNT(*)" +
|
||||||
" , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" +
|
" FROM TSEAIFR11" +
|
||||||
" , COUNT(*) AS CNT" +
|
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')")
|
||||||
" FROM TSEAIFR11 A" +
|
long countTokenInsufficient(@Param("rangeMinute") long rangeMinute);
|
||||||
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')" +
|
|
||||||
" GROUP BY EAISVCNAME")
|
|
||||||
List<InflowTokenInsufficient> countTokenInsufficient(@Param("rangeMinute") long rangeMinute);
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.eactive.eai.rms.ext.djb.inflow;
|
package com.eactive.eai.rms.ext.djb.inflow;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -11,7 +10,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||||
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
|
|
||||||
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
||||||
|
|
||||||
|
|
||||||
@@ -31,17 +29,21 @@ public class InflowTokenService {
|
|||||||
private UmsManager ums;
|
private UmsManager ums;
|
||||||
|
|
||||||
public void checkRecentFails(long rangeMinute) {
|
public void checkRecentFails(long rangeMinute) {
|
||||||
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
|
if (!monitoringContext.getBooleanProperty(MonitoringContext.DJB_UMS_MESSENGER_APIMONITOR_ENABLED, true)) {
|
||||||
|
return;
|
||||||
for (InflowTokenInsufficient info : rows) {
|
|
||||||
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
|
|
||||||
String message = "유량제어 토큰 획득 실패가 발생하였습니다 \n" + info.getEaisvcname();
|
|
||||||
|
|
||||||
if (monitoringContext.getBooleanProperty(MonitoringContext.DJB_UMS_MESSENGER_APIMONITOR_ENABLED, true)) {
|
|
||||||
HashMap<String,Object> params = new HashMap<String,Object>();
|
|
||||||
params.put("message", message);
|
|
||||||
ums.send("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, params);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long failCount = inflowTokenRepository.countTokenInsufficient(rangeMinute);
|
||||||
|
if (failCount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("유량제어 토큰 획득 실패: 최근 {}분 동안 {}건", rangeMinute, failCount);
|
||||||
|
|
||||||
|
String message = "유량제어 토큰 획득 실패가 발생하였습니다\n최근 " + rangeMinute + "분 동안 " + failCount + "건";
|
||||||
|
|
||||||
|
HashMap<String, Object> params = new HashMap<String, Object>();
|
||||||
|
params.put("message", message);
|
||||||
|
ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,9 +126,9 @@ public class ApiStatsHourlyAggregationJob implements Job {
|
|||||||
" , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
|
" , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
|
||||||
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
|
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
|
||||||
" , COUNT(EAISVCSERNO)" +
|
" , COUNT(EAISVCSERNO)" +
|
||||||
" , SUM(CASE WHEN (A.ERROR_CODE IS NULL OR C.CODE IS NOT NULL) AND E400 IS NOT NULL THEN 1 ELSE 0 END)" +
|
" , SUM(CASE WHEN A.ERROR_CODE IS NULL OR C.CODE IS NOT NULL THEN 1 ELSE 0 END)" +
|
||||||
" , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" +
|
" , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" +
|
||||||
" , SUM(CASE WHEN (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.CODE IS NULL AND C.CODE IS NULL THEN 1 ELSE 0 END)" +
|
" , SUM(CASE WHEN A.ERROR_CODE IS NOT NULL AND B.CODE IS NULL AND C.CODE IS NULL THEN 1 ELSE 0 END)" +
|
||||||
" , 0, 0, 0, 0" +
|
" , 0, 0, 0, 0" +
|
||||||
" , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" +
|
" , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" +
|
||||||
" FROM (" +
|
" FROM (" +
|
||||||
@@ -143,8 +143,7 @@ public class ApiStatsHourlyAggregationJob implements Job {
|
|||||||
" + TO_NUMBER(SUBSTR(MAX(MSGPRCSSYMS),15,3))" +
|
" + TO_NUMBER(SUBSTR(MAX(MSGPRCSSYMS),15,3))" +
|
||||||
" - TO_NUMBER(SUBSTR(MIN(MSGPRCSSYMS),15,3))" +
|
" - TO_NUMBER(SUBSTR(MIN(MSGPRCSSYMS),15,3))" +
|
||||||
" ) AS RESP_TIME" +
|
" ) AS RESP_TIME" +
|
||||||
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" +
|
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN EAIERRCD ELSE '' END) AS ERROR_CODE" +
|
||||||
" , MAX(EAIERRCD) AS ERROR_CODE" +
|
|
||||||
" FROM " + logTableName + " A " +
|
" FROM " + logTableName + " A " +
|
||||||
" WHERE MSGDPSTYMS >= :rangeStart AND MSGDPSTYMS < :rangeEnd" +
|
" WHERE MSGDPSTYMS >= :rangeStart AND MSGDPSTYMS < :rangeEnd" +
|
||||||
" GROUP BY EAISVCSERNO" +
|
" GROUP BY EAISVCSERNO" +
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.eactive.eai.rms.ext.djb.job;
|
package com.eactive.eai.rms.ext.djb.job;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -13,7 +11,6 @@ import org.quartz.JobDataMap;
|
|||||||
import org.quartz.JobExecutionContext;
|
import org.quartz.JobExecutionContext;
|
||||||
import org.quartz.JobExecutionException;
|
import org.quartz.JobExecutionException;
|
||||||
import org.quartz.SchedulerException;
|
import org.quartz.SchedulerException;
|
||||||
import org.quartz.Trigger;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
@@ -37,7 +34,7 @@ import com.eactive.eai.rms.onl.common.util.DateUtil;
|
|||||||
* <p>Job 파라미터 (JobDataMap):</p>
|
* <p>Job 파라미터 (JobDataMap):</p>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>
|
* <li>
|
||||||
* NONE
|
* <b>api.inflow.fail.range_minute</b>: 유량제어 토큰 획득 실패 조회 구간 (단위: 분, 기본값: 5)
|
||||||
* </li>
|
* </li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
@@ -48,6 +45,12 @@ public class InflowTokenMonitorJob implements Job {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(InflowTokenMonitorJob.class);
|
private static final Logger log = LoggerFactory.getLogger(InflowTokenMonitorJob.class);
|
||||||
|
|
||||||
|
/** Job 파라미터 키: 유량제어 실패 조회 구간(분) */
|
||||||
|
public static final String KEY_API_INFLOW_FAIL_RANGE_MINUTE = "api.inflow.fail.range_minute";
|
||||||
|
|
||||||
|
/** 유량제어 실패 조회 구간 기본값(분) */
|
||||||
|
public static final String DEFAULT_RANGE_MINUTE = "5";
|
||||||
|
|
||||||
private transient MonitoringContext monitoringContext;
|
private transient MonitoringContext monitoringContext;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -69,18 +72,19 @@ public class InflowTokenMonitorJob implements Job {
|
|||||||
|
|
||||||
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||||
|
|
||||||
long execMinute = 1; //default 배치 실행주기(분)
|
// Job 프로퍼티에서 유량제어 실패 조회 구간(분)을 읽는다. 미지정 시 기본값 사용
|
||||||
|
long execMinute;
|
||||||
Trigger trigger = context.getTrigger();
|
String rangeMinute = jobDataMap.getString(KEY_API_INFLOW_FAIL_RANGE_MINUTE);
|
||||||
Date prevFireTime = trigger.getPreviousFireTime();
|
if (StringUtils.isEmpty(rangeMinute)) {
|
||||||
Date nextFireTime = trigger.getNextFireTime();
|
rangeMinute = DEFAULT_RANGE_MINUTE;
|
||||||
|
|
||||||
//1회 이상 배치가 실행된 경우, 배치 간격을 구하여 유량제어조회 구간 파라미터로 전달한다
|
|
||||||
if (prevFireTime != null && nextFireTime != null) {
|
|
||||||
long diffMillis = nextFireTime.getTime() - prevFireTime.getTime();
|
|
||||||
execMinute = TimeUnit.MILLISECONDS.toMinutes(diffMillis);
|
|
||||||
log.info("실행 주기: {}분", execMinute);
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
execMinute = Long.parseLong(rangeMinute.trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("잘못된 {} 값: {} - 기본값 {} 사용", KEY_API_INFLOW_FAIL_RANGE_MINUTE, rangeMinute, DEFAULT_RANGE_MINUTE);
|
||||||
|
execMinute = Long.parseLong(DEFAULT_RANGE_MINUTE);
|
||||||
|
}
|
||||||
|
log.info("유량제어 실패 조회 구간: {}분", execMinute);
|
||||||
|
|
||||||
ApplicationContext appContext;
|
ApplicationContext appContext;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -57,19 +57,19 @@ public class ApiUseStatsService
|
|||||||
|
|
||||||
String dataSql = "";
|
String dataSql = "";
|
||||||
if ("ORG".equals(search.getSearchType())) {
|
if ("ORG".equals(search.getSearchType())) {
|
||||||
dataSql = "SELECT NVL(NVL(B.ORGNAME, D.ORG_NAME),'미분류') AS ORGNAME";
|
dataSql = "SELECT NVL(NVL(B.ORGNAME, D.ORG_NAME),'미분류') AS ORGNAME, '' AS APIDESC";
|
||||||
dataSql += getQueryBody(search);
|
dataSql += getQueryBody(search);
|
||||||
dataSql += " GROUP BY NVL(NVL(B.ORGNAME, D.ORG_NAME),'미분류')";
|
dataSql += " GROUP BY NVL(NVL(B.ORGNAME, D.ORG_NAME),'미분류')";
|
||||||
dataSql += " ORDER BY ORGNAME";
|
dataSql += " ORDER BY ORGNAME";
|
||||||
}
|
}
|
||||||
else if ("API".equals(search.getSearchType())) {
|
else if ("API".equals(search.getSearchType())) {
|
||||||
dataSql = "SELECT A.API_NAME ";
|
dataSql = "SELECT A.API_NAME, A.EAISVCDESC ";
|
||||||
dataSql += getQueryBody(search);
|
dataSql += getQueryBody(search);
|
||||||
dataSql += " GROUP BY A.API_NAME";
|
dataSql += " GROUP BY A.API_NAME, A.EAISVCDESC ";
|
||||||
dataSql += " ORDER BY API_NAME";
|
dataSql += " ORDER BY API_NAME";
|
||||||
}
|
}
|
||||||
else if ("DATE".equals(search.getSearchType())) {
|
else if ("DATE".equals(search.getSearchType())) {
|
||||||
dataSql = "SELECT TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME ";
|
dataSql = "SELECT TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME, '' AS APIDESC ";
|
||||||
dataSql += getQueryBody(search);
|
dataSql += getQueryBody(search);
|
||||||
dataSql += " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')";
|
dataSql += " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')";
|
||||||
dataSql += " ORDER BY STAT_TIME";
|
dataSql += " ORDER BY STAT_TIME";
|
||||||
@@ -85,7 +85,7 @@ public class ApiUseStatsService
|
|||||||
StringBuilder where = buildNativeWhere(search);
|
StringBuilder where = buildNativeWhere(search);
|
||||||
|
|
||||||
String totalSql;
|
String totalSql;
|
||||||
totalSql = "SELECT '합계' AS ORGNAME ";
|
totalSql = "SELECT '합계' AS ORGNAME, '' AS APIDESC ";
|
||||||
totalSql += getQueryBody(search);
|
totalSql += getQueryBody(search);
|
||||||
|
|
||||||
Query totalQuery = entityManager.createNativeQuery(totalSql);
|
Query totalQuery = entityManager.createNativeQuery(totalSql);
|
||||||
@@ -109,6 +109,7 @@ public class ApiUseStatsService
|
|||||||
+ " FROM ("
|
+ " FROM ("
|
||||||
+ " SELECT A.STAT_TIME "
|
+ " SELECT A.STAT_TIME "
|
||||||
+ " , A.API_NAME"
|
+ " , A.API_NAME"
|
||||||
|
+ " , C.EAISVCDESC"
|
||||||
+ " , A.CLIENT_ID "
|
+ " , A.CLIENT_ID "
|
||||||
+ " , A.TOTAL_CNT"
|
+ " , A.TOTAL_CNT"
|
||||||
+ " , A.SUCCESS_CNT"
|
+ " , A.SUCCESS_CNT"
|
||||||
@@ -118,13 +119,13 @@ public class ApiUseStatsService
|
|||||||
+ " , A.MIN_RESP_TIME"
|
+ " , A.MIN_RESP_TIME"
|
||||||
+ " , A.MAX_RESP_TIME"
|
+ " , A.MAX_RESP_TIME"
|
||||||
+ " , CASE WHEN API_NAME LIKE '%1' THEN SUBSTR(OUTBOUND_ADAPTER,2,3) ELSE SUBSTR(INBOUND_ADAPTER,2,3) END AS ORG_CODE"
|
+ " , CASE WHEN API_NAME LIKE '%1' THEN SUBSTR(OUTBOUND_ADAPTER,2,3) ELSE SUBSTR(INBOUND_ADAPTER,2,3) END AS ORG_CODE"
|
||||||
+ " FROM API_STATS_DAY A "
|
+ " FROM API_STATS_DAY A LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
|
||||||
+ " WHERE A.STAT_TIME >= :searchStartDate "
|
+ " WHERE A.STAT_TIME >= :searchStartDate "
|
||||||
+ " AND A.STAT_TIME <= :searchEndDate ";
|
+ " AND A.STAT_TIME <= :searchEndDate ";
|
||||||
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(search.getSearchApiName())) {
|
if (StringUtils.isNotBlank(search.getSearchApiName())) {
|
||||||
dataSql += " AND A.API_NAME LIKE :searchApiName ";
|
dataSql += " AND UPPER(A.API_NAME) LIKE :searchApiName ";
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSql += " ) A"
|
dataSql += " ) A"
|
||||||
@@ -160,15 +161,16 @@ public class ApiUseStatsService
|
|||||||
private ApiStatsUI toVO(Object[] row) {
|
private ApiStatsUI toVO(Object[] row) {
|
||||||
ApiStatsUI vo = new ApiStatsUI();
|
ApiStatsUI vo = new ApiStatsUI();
|
||||||
vo.setOrgName(StringUtils.toString(row[0]));
|
vo.setOrgName(StringUtils.toString(row[0]));
|
||||||
vo.setTotalCnt(StringUtils.toLong(row[1]));
|
vo.setApiDesc(StringUtils.toString(row[1]));
|
||||||
vo.setSuccessCnt(StringUtils.toLong(row[2]));
|
vo.setTotalCnt(StringUtils.toLong(row[2]));
|
||||||
vo.setSuccessRate(StringUtils.toDecimal(row[3]).setScale(2, RoundingMode.HALF_UP));
|
vo.setSuccessCnt(StringUtils.toLong(row[3]));
|
||||||
vo.setFailRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP));
|
vo.setSuccessRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP));
|
||||||
vo.setTimeoutCnt(StringUtils.toLong(row[5]));
|
vo.setFailRate(StringUtils.toDecimal(row[5]).setScale(2, RoundingMode.HALF_UP));
|
||||||
vo.setSystemErrCnt(StringUtils.toLong(row[6]));
|
vo.setTimeoutCnt(StringUtils.toLong(row[6]));
|
||||||
vo.setAvgRespTime(StringUtils.toDecimal(row[7]));
|
vo.setSystemErrCnt(StringUtils.toLong(row[7]));
|
||||||
vo.setMinRespTime(StringUtils.toDecimal(row[8]));
|
vo.setAvgRespTime(StringUtils.toDecimal(row[8]));
|
||||||
vo.setMaxRespTime(StringUtils.toDecimal(row[9]));
|
vo.setMinRespTime(StringUtils.toDecimal(row[9]));
|
||||||
|
vo.setMaxRespTime(StringUtils.toDecimal(row[10]));
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package com.eactive.eai.rms.ext.djb.ums.service;
|
package com.eactive.eai.rms.ext.djb.ums.service;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -40,11 +44,9 @@ public class UmsService {
|
|||||||
private static final Gson gson = new Gson();
|
private static final Gson gson = new Gson();
|
||||||
|
|
||||||
private static final String SYS_DVCD = "API"; //시스템코드
|
private static final String SYS_DVCD = "API"; //시스템코드
|
||||||
private static final String BZWK_DVCD = "99"; //업무구분코드
|
|
||||||
private static final String BZWK_CD = "999"; //업무코드
|
|
||||||
private static final String BZWK_DCLS_DVCD = "9019"; //세부구분코드
|
private static final String BZWK_DCLS_DVCD = "9019"; //세부구분코드
|
||||||
private static final String SUB_BZWK_CD = "999"; //서브업무코드 TODO: 변경할것
|
private static final String BZWK_CD = "EBK"; //업무코드
|
||||||
private static final String TEAM_DVCD = "99"; //팀구분코드 TODO: 변경해야함
|
private static final String SUB_BZWK_CD = "EBG"; //서브업무코드
|
||||||
|
|
||||||
|
|
||||||
private final MonitoringPropertyService propertyService;
|
private final MonitoringPropertyService propertyService;
|
||||||
@@ -60,13 +62,21 @@ public class UmsService {
|
|||||||
throw new IllegalArgumentException("messageRequest is null");
|
throw new IllegalArgumentException("messageRequest is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.httpConnection(messageRequest);
|
UmsResponse response = this.httpConnection(messageRequest);
|
||||||
|
|
||||||
return true;
|
// HTTP 응답코드가 2xx 일 때만 SENT, 나머지는 FAILED
|
||||||
|
boolean success = response.getHttpStatus() >= 200 && response.getHttpStatus() < 300;
|
||||||
|
|
||||||
|
messageRequest.setRequestStatus(success ? "SENT" : "FAILED");
|
||||||
|
messageRequest.setSentDate(LocalDateTime.now());
|
||||||
|
messageRequest.setResponseData(response.getResponseData());
|
||||||
|
messageRequestService.save(messageRequest);
|
||||||
|
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void httpConnection(MessageRequest messageRequest) throws Exception {
|
private UmsResponse httpConnection(MessageRequest messageRequest) throws Exception {
|
||||||
HttpURLConnection conn = null;
|
HttpURLConnection conn = null;
|
||||||
BufferedReader br = null;
|
BufferedReader br = null;
|
||||||
log.debug("<<<HTTP Connection>>>:" + messageRequest.toString());
|
log.debug("<<<HTTP Connection>>>:" + messageRequest.toString());
|
||||||
@@ -99,26 +109,31 @@ public class UmsService {
|
|||||||
os.close();
|
os.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("getContentType();" + conn.getContentType()); // 응답 콘텐츠 유형 구하기
|
log.debug("getContentType();" + conn.getContentType()); // 응답 콘텐츠 유형 구하기
|
||||||
log.debug("getResponsecode();" + conn.getResponseCode()); // 응답 코드 구하기
|
log.debug("getResponsecode();" + responseCode); // 응답 코드 구하기
|
||||||
log.debug("getResponseMessage():" + conn.getResponseMessage()); // 응답 메세지 구하기
|
log.debug("getResponseMessage():" + conn.getResponseMessage()); // 응답 메세지 구하기
|
||||||
}
|
}
|
||||||
|
|
||||||
// http 요청 후 응답 받은 데이타를 버퍼에 쌓는다
|
// http 요청 후 응답 받은 데이타를 버퍼에 쌓는다
|
||||||
if (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
|
InputStream is = (responseCode >= 200 && responseCode < 300) ? conn.getInputStream() : conn.getErrorStream();
|
||||||
br = new BufferedReader(new InputStreamReader(conn.getInputStream(), charset));
|
if (is != null) {
|
||||||
} else {
|
br = new BufferedReader(new InputStreamReader(is, charset));
|
||||||
br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), charset));
|
|
||||||
|
String inputLine;
|
||||||
|
sb = new StringBuffer();
|
||||||
|
while ((inputLine = br.readLine()) != null) {
|
||||||
|
sb.append(inputLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData = sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
String inputLine;
|
|
||||||
sb = new StringBuffer();
|
|
||||||
while ((inputLine = br.readLine()) != null) {
|
|
||||||
sb.append(inputLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
responseData = sb.toString();
|
|
||||||
|
return new UmsResponse(responseCode, responseData);
|
||||||
} finally {
|
} finally {
|
||||||
// http 요청 및 응답 완료 후 BufferedReader를 닫는다
|
// http 요청 및 응답 완료 후 BufferedReader를 닫는다
|
||||||
if (br != null) {
|
if (br != null) {
|
||||||
@@ -130,7 +145,64 @@ public class UmsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** UMS HTTP 호출 결과(응답코드 + 응답본문) 보관용. */
|
||||||
|
private static class UmsResponse {
|
||||||
|
private final int httpStatus;
|
||||||
|
private final String responseData;
|
||||||
|
|
||||||
|
UmsResponse(int httpStatus, String responseData) {
|
||||||
|
this.httpStatus = httpStatus;
|
||||||
|
this.responseData = responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getHttpStatus() {
|
||||||
|
return httpStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getResponseData() {
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 표준 헤더의 최초전문송신IP(frst_trnm_ipad)용 - 프로퍼티 설정값 대신 현재 서버(WAS)의 실제 IP를 조회한다.
|
||||||
|
* 조회 실패 시에는 기존처럼 djb.ums.was_ip_address 프로퍼티 값으로 대체한다.
|
||||||
|
*/
|
||||||
|
private String getCurrentServerIp() {
|
||||||
|
try {
|
||||||
|
return InetAddress.getLocalHost().getHostAddress();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("getCurrentServerIp error", e);
|
||||||
|
return propertyService.getPropertyValue("Monitoring", "djb.ums.was_ip_address", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 표준 헤더의 최초전문송신MAC(frst_trnm_mac)용 - 프로퍼티 설정값 대신 현재 서버(WAS)의 실제 MAC 주소를 조회한다.
|
||||||
|
* 조회 실패(네트워크 인터페이스를 찾지 못하는 경우 포함) 시에는 기존처럼 djb.ums.was_mac_address 프로퍼티 값으로 대체한다.
|
||||||
|
*/
|
||||||
|
private String getCurrentServerMac() {
|
||||||
|
try {
|
||||||
|
NetworkInterface ni = NetworkInterface.getByInetAddress(InetAddress.getLocalHost());
|
||||||
|
byte[] mac = (ni != null) ? ni.getHardwareAddress() : null;
|
||||||
|
if (mac == null) {
|
||||||
|
return propertyService.getPropertyValue("Monitoring", "djb.ums.was_mac_address", "");
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < mac.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append(":");
|
||||||
|
}
|
||||||
|
sb.append(String.format("%02X", mac[i]));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("getCurrentServerMac error", e);
|
||||||
|
return propertyService.getPropertyValue("Monitoring", "djb.ums.was_mac_address", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String makeBodyData(MessageRequest messageRequest) {
|
private String makeBodyData(MessageRequest messageRequest) {
|
||||||
// 리턴값
|
// 리턴값
|
||||||
@@ -147,9 +219,9 @@ public class UmsService {
|
|||||||
stdHeadVO.setStnd_mesg_ver("R10");
|
stdHeadVO.setStnd_mesg_ver("R10");
|
||||||
stdHeadVO.setOrtr_guid(jsonData[2]);
|
stdHeadVO.setOrtr_guid(jsonData[2]);
|
||||||
stdHeadVO.setSect_ecrpt_yn("N");
|
stdHeadVO.setSect_ecrpt_yn("N");
|
||||||
stdHeadVO.setFrst_trnm_ipad(propertyService.getPropertyValue("Monitoring", "djb.ums.was_ip_address", ""));
|
stdHeadVO.setFrst_trnm_ipad(getCurrentServerIp());
|
||||||
stdHeadVO.setFrst_trnm_ipv4_addr(propertyService.getPropertyValue("Monitoring", "djb.ums.was_ip_address", ""));
|
stdHeadVO.setFrst_trnm_ipv4_addr(getCurrentServerIp());
|
||||||
stdHeadVO.setFrst_trnm_mac(propertyService.getPropertyValue("Monitoring", "djb.ums.was_mac_address", ""));
|
stdHeadVO.setFrst_trnm_mac(getCurrentServerMac());
|
||||||
stdHeadVO.setFrst_mesg_dman_dt(jsonData[0]);
|
stdHeadVO.setFrst_mesg_dman_dt(jsonData[0]);
|
||||||
stdHeadVO.setFrst_mesg_dman_time(jsonData[1]);
|
stdHeadVO.setFrst_mesg_dman_time(jsonData[1]);
|
||||||
stdHeadVO.setFrst_trnm_sys_dvcd(SYS_DVCD);
|
stdHeadVO.setFrst_trnm_sys_dvcd(SYS_DVCD);
|
||||||
@@ -161,7 +233,7 @@ public class UmsService {
|
|||||||
stdHeadVO.setChnl_tycd("EAI"); // 채널유형코드
|
stdHeadVO.setChnl_tycd("EAI"); // 채널유형코드
|
||||||
stdHeadVO.setHmab_dvcd("1"); // 내외구분코드 1:내부 2:외부(타발)
|
stdHeadVO.setHmab_dvcd("1"); // 내외구분코드 1:내부 2:외부(타발)
|
||||||
stdHeadVO.setIf_id(messageRequest.getEaiInterfaceId()); // 인터페이스ID
|
stdHeadVO.setIf_id(messageRequest.getEaiInterfaceId()); // 인터페이스ID
|
||||||
stdHeadVO.setTx_id(messageRequest.getServiceId()); // 거래ID
|
stdHeadVO.setTx_id(messageRequest.getEaiTxId()); // 거래ID
|
||||||
|
|
||||||
if ("MESSENGER".equals(messageRequest.getMessageType()) ) {
|
if ("MESSENGER".equals(messageRequest.getMessageType()) ) {
|
||||||
|
|
||||||
@@ -182,7 +254,7 @@ public class UmsService {
|
|||||||
|
|
||||||
// 스윙챗 알림 데이터
|
// 스윙챗 알림 데이터
|
||||||
SwingDataVO swiDataVO = new SwingDataVO();
|
SwingDataVO swiDataVO = new SwingDataVO();
|
||||||
swiDataVO.setNotiCode("CNO_FEP_APIM_0002"); //0001:긴급, 0002:중요
|
swiDataVO.setNotiCode("CNO_FEP_API_0002"); //0001:긴급, 0002:중요
|
||||||
swiDataVO.setNotiTitle("알림");
|
swiDataVO.setNotiTitle("알림");
|
||||||
swiDataVO.setNotiContent(messageRequest.getMessage());
|
swiDataVO.setNotiContent(messageRequest.getMessage());
|
||||||
|
|
||||||
@@ -205,7 +277,7 @@ public class UmsService {
|
|||||||
// Sms 데이터
|
// Sms 데이터
|
||||||
SmsDataVO dataVO = new SmsDataVO();
|
SmsDataVO dataVO = new SmsDataVO();
|
||||||
dataVO.setSnd_dman_dt(jsonData[0]);
|
dataVO.setSnd_dman_dt(jsonData[0]);
|
||||||
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
|
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest));
|
||||||
dataVO.setSnd_dvcd("EAI");
|
dataVO.setSnd_dvcd("EAI");
|
||||||
dataVO.setSms_msg_titl(StringUtil.chunkString(messageRequest.getSubject(), 40, false)); // MS949 40byte 초과시 절단
|
dataVO.setSms_msg_titl(StringUtil.chunkString(messageRequest.getSubject(), 40, false)); // MS949 40byte 초과시 절단
|
||||||
dataVO.setSms_msg_ctnt(messageRequest.getMessage());
|
dataVO.setSms_msg_ctnt(messageRequest.getMessage());
|
||||||
@@ -215,19 +287,21 @@ public class UmsService {
|
|||||||
dataVO.setSnd_emp_dprm_cd("132"); //요청부서코드
|
dataVO.setSnd_emp_dprm_cd("132"); //요청부서코드
|
||||||
dataVO.setLtrs_snd_tycd("2"); // 1:SMS 2:LMS/MMS
|
dataVO.setLtrs_snd_tycd("2"); // 1:SMS 2:LMS/MMS
|
||||||
dataVO.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
|
dataVO.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
|
||||||
//ADMIN으로 시작하면 제주은행 임직원, 나머지는 제주은행 고객용
|
//ADMIN으로 시작하면 제주은행 내부용, 나머지는 제주은행 고객용
|
||||||
if (messageRequest.getMessageCode().name().startsWith("ADMIN")) {
|
if (messageRequest.getMessageCode().name().startsWith("ADMIN")) {
|
||||||
|
dataVO.setSms_trnm_bzwk_dvcd("99"); //업무구분코드
|
||||||
|
dataVO.setSms_bzwk_dcls_dvcd(BZWK_DCLS_DVCD); //업무세분코드
|
||||||
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key.intra", "")); // 제주은행 임직원 플러스친구아이디
|
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key.intra", "")); // 제주은행 임직원 플러스친구아이디
|
||||||
} else {
|
} else {
|
||||||
|
dataVO.setSms_trnm_bzwk_dvcd("95"); //업무구분코드
|
||||||
|
dataVO.setSms_bzwk_dcls_dvcd(BZWK_DCLS_DVCD); //업무세분코드
|
||||||
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key.public", "")); // 제주은행 플러스친구아이디
|
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key.public", "")); // 제주은행 플러스친구아이디
|
||||||
}
|
}
|
||||||
dataVO.setAlmtk_tmplt_cd(BZWK_DVCD + TEAM_DVCD + BZWK_DCLS_DVCD + "001"); // 템플릿코드
|
dataVO.setAlmtk_tmplt_cd(messageRequest.getServiceId()); // 템플릿코드
|
||||||
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
|
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
|
||||||
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
|
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
|
||||||
|
|
||||||
dataVO.setSms_trnm_bzwk_dvcd(BZWK_DVCD); //업무구분코드
|
dataVO.setSms_sys_dvcd("85"); //시스템코드
|
||||||
dataVO.setSms_bzwk_dcls_dvcd(BZWK_DCLS_DVCD); //업무세분코드
|
|
||||||
dataVO.setSms_sys_dvcd("83"); //시스템코드
|
|
||||||
dataVO.setTx_dt(jsonData[0]); //거래일자
|
dataVO.setTx_dt(jsonData[0]); //거래일자
|
||||||
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
|
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
|
||||||
|
|
||||||
@@ -251,7 +325,7 @@ public class UmsService {
|
|||||||
EmailDataVO dataVO = new EmailDataVO();
|
EmailDataVO dataVO = new EmailDataVO();
|
||||||
dataVO.setUms_evnt_id(propertyService.getPropertyValue("Monitoring", "djb.ums.email.ums_evnt_id", "")); // UMS이벤트ID(필수)
|
dataVO.setUms_evnt_id(propertyService.getPropertyValue("Monitoring", "djb.ums.email.ums_evnt_id", "")); // UMS이벤트ID(필수)
|
||||||
dataVO.setSnd_dman_dt(jsonData[0]);
|
dataVO.setSnd_dman_dt(jsonData[0]);
|
||||||
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
|
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest));
|
||||||
dataVO.setDman_seqno(1); // 발송요청순번
|
dataVO.setDman_seqno(1); // 발송요청순번
|
||||||
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
|
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
|
||||||
dataVO.setCstno(propertyService.getPropertyValue("Monitoring", "djb.ums.email.cstno", "")); // 고객번호(필수)
|
dataVO.setCstno(propertyService.getPropertyValue("Monitoring", "djb.ums.email.cstno", "")); // 고객번호(필수)
|
||||||
@@ -263,7 +337,7 @@ public class UmsService {
|
|||||||
dataVO.setSnd_emp_dprm_cd("132"); // 발신자부서코드(필수)
|
dataVO.setSnd_emp_dprm_cd("132"); // 발신자부서코드(필수)
|
||||||
dataVO.setBzwk_cd(BZWK_CD); // 업무코드
|
dataVO.setBzwk_cd(BZWK_CD); // 업무코드
|
||||||
dataVO.setSub_bzwk_cd(SUB_BZWK_CD); // 서브업무코드
|
dataVO.setSub_bzwk_cd(SUB_BZWK_CD); // 서브업무코드
|
||||||
dataVO.setSys_dvcd(SYS_DVCD); // 시스템구분코드
|
dataVO.setSys_dvcd("85"); // 시스템구분코드
|
||||||
dataVO.setTx_dt(jsonData[0]); // 거래일자
|
dataVO.setTx_dt(jsonData[0]); // 거래일자
|
||||||
dataVO.setTx_tktm(jsonData[1].substring(0,6)); // 거래시간
|
dataVO.setTx_tktm(jsonData[1].substring(0,6)); // 거래시간
|
||||||
|
|
||||||
@@ -357,20 +431,24 @@ public class UmsService {
|
|||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getSndDmanId(String messageType) {
|
private String getSndDmanId(MessageRequest messageRequest) {
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
|
||||||
String yyyymmdd = sdf.format(new Date());
|
String yyyymmdd = sdf.format(new Date());
|
||||||
|
|
||||||
String uuid = "";
|
String uuid = "";
|
||||||
|
|
||||||
if ("EMAIL".equals(messageType)) {
|
if ("EMAIL".equals(messageRequest.getMessageType())) {
|
||||||
uuid = SYS_DVCD + BZWK_CD + SUB_BZWK_CD + yyyymmdd + "O" + "E" ;
|
uuid = SYS_DVCD + BZWK_CD + SUB_BZWK_CD + yyyymmdd + "O" + "E" ;
|
||||||
|
|
||||||
long seq = messageRequestService.countEmailRequestsOnDate(LocalDate.now()) + 1;
|
long seq = messageRequestService.countEmailRequestsOnDate(LocalDate.now()) + 1;
|
||||||
uuid += String.format("%013d", seq);
|
uuid += String.format("%013d", seq);
|
||||||
|
|
||||||
} else if ("SMS".equals(messageType)) { //kakao 알림톡
|
} else if ("SMS".equals(messageRequest.getMessageType())) { //kakao 알림톡
|
||||||
uuid = SYS_DVCD + BZWK_DVCD + BZWK_DCLS_DVCD + yyyymmdd + "O" + "K";
|
if (messageRequest.getMessageCode().name().startsWith("ADMIN")) {
|
||||||
|
uuid = SYS_DVCD + "99" + BZWK_DCLS_DVCD + yyyymmdd + "O" + "K"; //내부용
|
||||||
|
} else {
|
||||||
|
uuid = SYS_DVCD + "95" + BZWK_DCLS_DVCD + yyyymmdd + "O" + "K"; //대고객
|
||||||
|
}
|
||||||
|
|
||||||
long seq = messageRequestService.countSmsKakaoRequestsOnDate(LocalDate.now()) + 1;
|
long seq = messageRequestService.countSmsKakaoRequestsOnDate(LocalDate.now()) + 1;
|
||||||
uuid += String.format("%013d", seq);
|
uuid += String.format("%013d", seq);
|
||||||
|
|||||||
@@ -245,6 +245,12 @@ public class WebhookService {
|
|||||||
Exception lastException = null;
|
Exception lastException = null;
|
||||||
for (int attempt = 0; attempt <= retryCount; attempt++) {
|
for (int attempt = 0; attempt <= retryCount; attempt++) {
|
||||||
try {
|
try {
|
||||||
|
if (attempt == 0) {
|
||||||
|
// 배포 반영 확인용: HttpComponentsClientHttpRequestFactory 여야 SSL 우회 RestTemplate 사용 중.
|
||||||
|
// SimpleClientHttpRequestFactory 로 찍히면 기본 new RestTemplate() 이 주입된 것 → PKIX 원인.
|
||||||
|
log.info("[Webhook] POST 시도 - url: {}, requestFactory: {}", proxyUrl,
|
||||||
|
restTemplate.getRequestFactory().getClass().getSimpleName());
|
||||||
|
}
|
||||||
if (attempt > 0) {
|
if (attempt > 0) {
|
||||||
long delayMs = (long) retryTime * attempt;
|
long delayMs = (long) retryTime * attempt;
|
||||||
log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, proxyUrl, delayMs);
|
log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, proxyUrl, delayMs);
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import org.springframework.web.client.RestTemplate;
|
|||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
public class RestTemplateConfiguration {
|
public class RestTemplateConfiguration {
|
||||||
|
|
||||||
@@ -36,6 +39,9 @@ public class RestTemplateConfiguration {
|
|||||||
.setMaxConnPerRoute(20)
|
.setMaxConnPerRoute(20)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// 배포 반영 확인용: 이 줄이 기동 로그에 없으면 수정 전 클래스가 떠 있는 것.
|
||||||
|
log.info("[RestTemplateConfiguration] httpClient 생성: SSL 인증서 체인/호스트명 검증 비활성화(TrustAllStrategy+NoopHostnameVerifier) 적용");
|
||||||
|
|
||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +56,7 @@ public class RestTemplateConfiguration {
|
|||||||
public RestTemplate restTemplate(HttpComponentsClientHttpRequestFactory factory) {
|
public RestTemplate restTemplate(HttpComponentsClientHttpRequestFactory factory) {
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
restTemplate.setRequestFactory(factory);
|
restTemplate.setRequestFactory(factory);
|
||||||
|
log.info("[RestTemplateConfiguration] RestTemplate 빈 생성: requestFactory={}", factory.getClass().getSimpleName());
|
||||||
return restTemplate;
|
return restTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
package com.eactive.eai.rms.onl.apim.approval.credential;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
|
||||||
|
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
||||||
|
import com.eactive.apim.portal.app.entity.Credential;
|
||||||
|
import com.eactive.eai.data.entity.onl.authserver.ClientEntity;
|
||||||
|
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.ApiSpecManService;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.DjbApiSpecAutoGenService;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APIGW 인증 클라이언트(TSEAIAU01) → 개발자포탈(ptl_credential) 반영, 포털 쪽 쓰기 전담.
|
||||||
|
*
|
||||||
|
* {@link com.eactive.eai.rms.onl.apim.approval.app.GwClientSyncService}(포털→GW 방향)의 반대 방향 대응물.
|
||||||
|
* 트랜잭션 매니저를 EMS(MONITORING 스키마 고정)로 명시해, 호출부(APIGW 트랜잭션)의
|
||||||
|
* {@code DataSourceContextHolder} 를 건드리지 않는다.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PortalCredentialSyncService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PortalCredentialSyncService.class);
|
||||||
|
|
||||||
|
private static final String DEFAULT_APP_DESCRIPTION = "APIGW에서 반영된 인증정보";
|
||||||
|
|
||||||
|
private final CredentialService credentialService;
|
||||||
|
private final ApiSpecInfoService apiSpecInfoService;
|
||||||
|
private final ApiSpecManService apiSpecManService;
|
||||||
|
private final DjbApiSpecAutoGenService autoGenService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GW 클라이언트 정보를 포털 Credential 로 반영한다(없으면 신규, 있으면 갱신).
|
||||||
|
*
|
||||||
|
* @param gwClient APIGW 스키마에서 조회한 클라이언트(orgid 는 공백이 아니어야 한다 - 호출부 사전 검증)
|
||||||
|
* @return 반영 결과
|
||||||
|
*/
|
||||||
|
public Outcome apply(ClientEntity gwClient) {
|
||||||
|
String clientId = gwClient.getClientid();
|
||||||
|
String orgId = gwClient.getOrgid();
|
||||||
|
|
||||||
|
Credential existing = credentialService.findByClientidAndOrgid(clientId, orgId);
|
||||||
|
boolean isNew = existing == null;
|
||||||
|
Credential credential = isNew ? new Credential() : existing;
|
||||||
|
if (isNew) {
|
||||||
|
credential.setClientid(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyGwFields(gwClient, credential);
|
||||||
|
|
||||||
|
if (isNew && StringUtils.isBlank(credential.getAppDescription())) {
|
||||||
|
credential.setAppDescription(DEFAULT_APP_DESCRIPTION);
|
||||||
|
}
|
||||||
|
// appIconFileId / server 는 포털 고유 필드라 GW 에 대응값이 없다 - 기존 값 그대로 유지(신규면 null)
|
||||||
|
|
||||||
|
Outcome outcome = new Outcome(isNew);
|
||||||
|
credential.setApiList(resolveApiList(gwClient.getApiList(), outcome));
|
||||||
|
|
||||||
|
credentialService.save(credential);
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyGwFields(ClientEntity gwClient, Credential credential) {
|
||||||
|
credential.setClientname(gwClient.getClientname());
|
||||||
|
credential.setClientsecret(gwClient.getClientsecret());
|
||||||
|
credential.setScope(gwClient.getScope());
|
||||||
|
credential.setGranttypes(gwClient.getGranttypes());
|
||||||
|
credential.setAccesstokenvalidityseconds(gwClient.getAccesstokenvalidityseconds());
|
||||||
|
credential.setRefreshtokenvalidityseconds(gwClient.getRefreshtokenvalidityseconds());
|
||||||
|
credential.setAllowedips(gwClient.getAllowedips());
|
||||||
|
credential.setAuthorities(gwClient.getAuthorities());
|
||||||
|
credential.setRedirecturi(gwClient.getRedirecturi());
|
||||||
|
credential.setSecuritykey(gwClient.getSecuritykey());
|
||||||
|
credential.setAutoapprove(gwClient.getAutoapprove());
|
||||||
|
credential.setResourceids(gwClient.getResourceids());
|
||||||
|
credential.setOrgid(gwClient.getOrgid());
|
||||||
|
credential.setOrgname(gwClient.getOrgname());
|
||||||
|
credential.setDailytokenlimit(gwClient.getDailytokenlimit());
|
||||||
|
credential.setModifiedby(gwClient.getModifiedby());
|
||||||
|
credential.setModifiedon(gwClient.getModifiedon());
|
||||||
|
// appstatus 는 GW 값 그대로 복사(차단 상태도 포털에 반영). 공백/null 이면 정상(1)으로 간주.
|
||||||
|
credential.setAppstatus(StringUtils.isNotBlank(gwClient.getAppstatus()) ? gwClient.getAppstatus() : "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GW apiList 의 각 eaisvcname 을 포털 ApiSpecInfo(영속) 로 매핑, 미등록이면 자동생성 후 저장한다. */
|
||||||
|
private List<ApiSpecInfo> resolveApiList(List<EAIMessageEntity> gwApiList, Outcome outcome) {
|
||||||
|
List<ApiSpecInfo> result = new ArrayList<>();
|
||||||
|
if (gwApiList == null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (EAIMessageEntity gwApi : gwApiList) {
|
||||||
|
String apiId = gwApi.getEaisvcname();
|
||||||
|
ApiSpecInfo spec = apiSpecInfoService.findById(apiId).orElse(null);
|
||||||
|
if (spec == null) {
|
||||||
|
spec = createApiSpec(apiId, gwApi, outcome);
|
||||||
|
}
|
||||||
|
result.add(spec);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApiSpecInfo createApiSpec(String apiId, EAIMessageEntity gwApi, Outcome outcome) {
|
||||||
|
ApiSpecInfoUI ui;
|
||||||
|
try {
|
||||||
|
ui = autoGenService.generateApiSpecInfo(apiId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("API Spec 자동생성 실패, 최소 정보로 대체 생성 - apiId: {}", apiId, e);
|
||||||
|
ui = new ApiSpecInfoUI();
|
||||||
|
ui.setApiId(apiId);
|
||||||
|
ui.setApiName(StringUtils.isNotBlank(gwApi.getEaisvcdesc()) ? gwApi.getEaisvcdesc() : apiId);
|
||||||
|
ui.setApiSimpleDescription(gwApi.getEaisvcdesc());
|
||||||
|
outcome.specGenFailed.add(apiId);
|
||||||
|
}
|
||||||
|
ui.setDisplayYn("N"); // 비공개로 자동 생성 - 관리자가 API Spec 관리에서 확인 후 공개
|
||||||
|
apiSpecManService.save(ui);
|
||||||
|
outcome.createdApiSpecs.add(apiId);
|
||||||
|
|
||||||
|
return apiSpecInfoService.findById(apiId)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("API Spec 저장 직후 조회 실패: " + apiId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class Outcome {
|
||||||
|
private final boolean inserted;
|
||||||
|
private final List<String> createdApiSpecs = new ArrayList<>();
|
||||||
|
private final List<String> specGenFailed = new ArrayList<>();
|
||||||
|
|
||||||
|
Outcome(boolean inserted) {
|
||||||
|
this.inserted = inserted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-83
@@ -6,7 +6,6 @@ import com.eactive.apim.portal.approval.statemachine.listener.ApprovalListener;
|
|||||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||||
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.ApprovalStatus;
|
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.ApprovalStatus;
|
||||||
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.OrgStatus;
|
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.OrgStatus;
|
||||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
|
||||||
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.template.entity.MessageCode;
|
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||||
@@ -14,27 +13,15 @@ import com.eactive.apim.portal.template.service.MessageRecipient;
|
|||||||
import com.eactive.apim.portal.template.service.MessageSendService;
|
import com.eactive.apim.portal.template.service.MessageSendService;
|
||||||
import com.eactive.eai.rms.data.entity.onl.apim.portalorg.PortalOrgService;
|
import com.eactive.eai.rms.data.entity.onl.apim.portalorg.PortalOrgService;
|
||||||
import com.eactive.eai.rms.data.entity.onl.apim.portaluser.PortalUserService;
|
import com.eactive.eai.rms.data.entity.onl.apim.portaluser.PortalUserService;
|
||||||
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUI;
|
|
||||||
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUIMapper;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.http.client.HttpClient;
|
|
||||||
import org.apache.http.client.config.RequestConfig;
|
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -42,36 +29,9 @@ import org.springframework.web.client.RestTemplate;
|
|||||||
public class PortalUserApprovalListener implements ApprovalListener {
|
public class PortalUserApprovalListener implements ApprovalListener {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(PortalUserApprovalListener.class);
|
private static final Logger logger = LoggerFactory.getLogger(PortalUserApprovalListener.class);
|
||||||
private static final String PORTAL_PROPERTY_GROUP = "Portal";
|
|
||||||
private final PortalPropertyService portalPropertyService;
|
|
||||||
private final PortalOrgService portalOrgService;
|
private final PortalOrgService portalOrgService;
|
||||||
private final PortalUserService portalUserService;
|
private final PortalUserService portalUserService;
|
||||||
private final MessageSendService messageSendService;
|
private final MessageSendService messageSendService;
|
||||||
private final PortalOrgUIMapper portalOrgUIMapper;
|
|
||||||
|
|
||||||
public static RestTemplate createRestTemplateWithTimeout(int connectTimeout) {
|
|
||||||
|
|
||||||
RequestConfig requestConfig = RequestConfig.custom()
|
|
||||||
.setConnectionRequestTimeout(connectTimeout)
|
|
||||||
.setConnectTimeout(connectTimeout)
|
|
||||||
.setSocketTimeout(connectTimeout)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpClient httpClient =
|
|
||||||
HttpClientBuilder.create()
|
|
||||||
.setMaxConnTotal(50)
|
|
||||||
.setMaxConnPerRoute(20)
|
|
||||||
.setDefaultRequestConfig(requestConfig)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
|
||||||
factory.setHttpClient(httpClient);
|
|
||||||
|
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
|
||||||
restTemplate.setRequestFactory(factory);
|
|
||||||
|
|
||||||
return restTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(Approval approval, Map<String, Object> options) throws ApprovalDeployException {
|
public void execute(Approval approval, Map<String, Object> options) throws ApprovalDeployException {
|
||||||
@@ -88,7 +48,6 @@ public class PortalUserApprovalListener implements ApprovalListener {
|
|||||||
portalOrg.setOrgStatus(OrgStatus.ACTIVE);
|
portalOrg.setOrgStatus(OrgStatus.ACTIVE);
|
||||||
portalOrgService.save(portalOrg);
|
portalOrgService.save(portalOrg);
|
||||||
|
|
||||||
// syncStaging(portalOrgUIMapper.toVo(portalOrg));
|
|
||||||
sendApprovalResult(portalUser);
|
sendApprovalResult(portalUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,48 +65,6 @@ public class PortalUserApprovalListener implements ApprovalListener {
|
|||||||
messageSendService.sendMessage(MessageCode.MANAGER_WITH_ORG_REGISTER_APPROVED, recipient, params);
|
messageSendService.sendMessage(MessageCode.MANAGER_WITH_ORG_REGISTER_APPROVED, recipient, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated 광주은행에서는 사용하지 않음. (개발/운영 완전 분리)
|
|
||||||
* @param portalOrgUI
|
|
||||||
* @throws ApprovalDeployException
|
|
||||||
*/
|
|
||||||
private void syncStaging(PortalOrgUI portalOrgUI) throws ApprovalDeployException {
|
|
||||||
throw new RuntimeException("Unable use this method(syncStaging())");
|
|
||||||
|
|
||||||
// // 프록시를 통한 스테이징 서버 호출 추가
|
|
||||||
// Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
|
|
||||||
// String apiKey = properties.get("apiKey");
|
|
||||||
// String proxyUrl = properties.get("portal.url");
|
|
||||||
// String timeout = properties.getOrDefault("deploy.proxy_timeout", "10000");
|
|
||||||
// String proxyEndpoint = proxyUrl + "/_onl/apim/portalorg/portalOrgMan.json";
|
|
||||||
//
|
|
||||||
// HttpHeaders headers = new HttpHeaders();
|
|
||||||
// headers.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
// headers.set("X-API-KEY", apiKey);
|
|
||||||
// headers.set("target_host", "stg");
|
|
||||||
// headers.set("action", "insert");
|
|
||||||
//
|
|
||||||
// portalOrgUI.setCompRegFile(null);
|
|
||||||
// HttpEntity<PortalOrgUI> request = new HttpEntity<>(portalOrgUI, headers);
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// ResponseEntity<String> response = createRestTemplateWithTimeout(Integer.parseInt(timeout)).exchange(
|
|
||||||
// proxyEndpoint,
|
|
||||||
// HttpMethod.POST,
|
|
||||||
// request,
|
|
||||||
// String.class
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// if (!response.getStatusCode().is2xxSuccessful()) {
|
|
||||||
// logger.error("Failed to sync with staging server. Status: {}, Body: {}", response.getStatusCode(), response.getBody());
|
|
||||||
// throw new ApprovalDeployException("기관 정보 배포에 실패 했습니다.");
|
|
||||||
// }
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// logger.error("Failed to sync with staging server. {}", e.getMessage());
|
|
||||||
// throw new ApprovalDeployException("기관 정보 배포에 실패 했습니다.");
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void rollback(Approval approval) {
|
public void rollback(Approval approval) {
|
||||||
logger.debug("Rollback started for approval ID: {}, type: {}, status: {}",
|
logger.debug("Rollback started for approval ID: {}, type: {}, status: {}",
|
||||||
|
|||||||
+1
@@ -72,6 +72,7 @@ public class MessageRequestManService extends BaseService {
|
|||||||
|
|
||||||
ui.setUmsUid(e.getUmsUid());
|
ui.setUmsUid(e.getUmsUid());
|
||||||
ui.setRequestStatus(e.getRequestStatus());
|
ui.setRequestStatus(e.getRequestStatus());
|
||||||
|
ui.setResponseData(e.getResponseData());
|
||||||
ui.setEaiInterfaceId(e.getEaiInterfaceId());
|
ui.setEaiInterfaceId(e.getEaiInterfaceId());
|
||||||
ui.setServiceId(e.getServiceId());
|
ui.setServiceId(e.getServiceId());
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ public class MessageRequestUI {
|
|||||||
/* 본문 — 패턴 마스킹 + span 색상강조된 안전 HTML */
|
/* 본문 — 패턴 마스킹 + span 색상강조된 안전 HTML */
|
||||||
private String messageHtml;
|
private String messageHtml;
|
||||||
|
|
||||||
|
/* UMS 응답 내용 (RESPONSE_DATA) — 원문 그대로, 화면에서는 text 로만 출력 */
|
||||||
|
private String responseData;
|
||||||
|
|
||||||
/* 수신자 (마스킹) */
|
/* 수신자 (마스킹) */
|
||||||
private String username;
|
private String username;
|
||||||
private String email;
|
private String email;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.eactive.eai.rms.onl.apim.portalnotice;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공지사항 등록 화면에 채워 넣을 장애 공지 양식 (제목 + 본문 HTML).
|
||||||
|
* 자동 탐지 초안과 같은 문구를 쓴다 - {@code ApiStatusNoticeComposer} 참조.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class NoticeDraftTemplateUI {
|
||||||
|
private String subject;
|
||||||
|
private String detail;
|
||||||
|
}
|
||||||
+32
@@ -6,7 +6,12 @@ import com.eactive.eai.rms.common.combo.ComboVo;
|
|||||||
import com.eactive.eai.rms.common.login.SessionManager;
|
import com.eactive.eai.rms.common.login.SessionManager;
|
||||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||||
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeUISearch;
|
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeUISearch;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
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.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
@@ -19,17 +24,23 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PortalNoticeManController extends BaseAnnotationController {
|
public class PortalNoticeManController extends BaseAnnotationController {
|
||||||
|
|
||||||
private final PortalNoticeManService portalNoticeManService;
|
private final PortalNoticeManService portalNoticeManService;
|
||||||
private final ComboService comboService;
|
private final ComboService comboService;
|
||||||
|
|
||||||
|
/** 영향 API 목록(JSON) 파싱 전용 - 화면에서 넘어온 값만 읽는다 */
|
||||||
|
private final ObjectMapper draftTemplateMapper = new ObjectMapper()
|
||||||
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
@GetMapping(value = "/onl/apim/portalnotice/portalNoticeMan.view")
|
@GetMapping(value = "/onl/apim/portalnotice/portalNoticeMan.view")
|
||||||
public void view() {
|
public void view() {
|
||||||
// view
|
// view
|
||||||
@@ -87,6 +98,27 @@ public class PortalNoticeManController extends BaseAnnotationController {
|
|||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 장애 공지 양식(제목·본문) 조회. 자동 탐지 초안과 같은 문구를 관리자 수동 등록에서도 쓴다.
|
||||||
|
*
|
||||||
|
* @param apisJson 화면에서 고른 영향 API 배열 - {@code [{"apiId":"...","apiName":"..."}]}.
|
||||||
|
* 이름에 쉼표가 들어갈 수 있어 구분자 문자열 대신 JSON 으로 받는다.
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=DRAFT_TEMPLATE")
|
||||||
|
public ResponseEntity<NoticeDraftTemplateUI> draftTemplate(String apisJson) {
|
||||||
|
List<IncidentAffectedApiUI> affectedApis = Collections.emptyList();
|
||||||
|
if (StringUtils.isNotBlank(apisJson)) {
|
||||||
|
try {
|
||||||
|
affectedApis = draftTemplateMapper.readValue(apisJson,
|
||||||
|
new TypeReference<List<IncidentAffectedApiUI>>() {});
|
||||||
|
} catch (IOException e) {
|
||||||
|
// 양식 채우기는 보조 기능이므로 파싱 실패 시 영향 API 없이 양식만 돌려준다
|
||||||
|
log.warn("영향 API 목록 파싱 실패 - 영향 API 없이 양식 생성", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(portalNoticeManService.buildDraftTemplate(affectedApis));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=TIMELINE_LIST")
|
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=TIMELINE_LIST")
|
||||||
public ResponseEntity<List<IncidentTimelineUI>> selectTimeline(Long incidentId) {
|
public ResponseEntity<List<IncidentTimelineUI>> selectTimeline(Long incidentId) {
|
||||||
return ResponseEntity.ok(portalNoticeManService.selectTimeline(incidentId));
|
return ResponseEntity.ok(portalNoticeManService.selectTimeline(incidentId));
|
||||||
|
|||||||
+40
-2
@@ -16,6 +16,8 @@ import com.eactive.apim.portal.file.service.FileTypeContext;
|
|||||||
import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
|
import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
|
||||||
import com.eactive.eai.common.util.ContainerUtil;
|
import com.eactive.eai.common.util.ContainerUtil;
|
||||||
import com.eactive.eai.rms.common.base.BaseService;
|
import com.eactive.eai.rms.common.base.BaseService;
|
||||||
|
import com.eactive.eai.rms.ext.djb.apistatus.ApiStatusDraftTemplate;
|
||||||
|
import com.eactive.eai.rms.ext.djb.apistatus.ApiStatusNoticeComposer;
|
||||||
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeService;
|
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeService;
|
||||||
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeUISearch;
|
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeUISearch;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -51,8 +53,16 @@ public class PortalNoticeManService extends BaseService {
|
|||||||
private final DjbApistatusIncidentRepository incidentRepository;
|
private final DjbApistatusIncidentRepository incidentRepository;
|
||||||
private final DjbApistatusIncidentApiRepository incidentApiRepository;
|
private final DjbApistatusIncidentApiRepository incidentApiRepository;
|
||||||
private final DjbApistatusIncidentTimelineRepository incidentTimelineRepository;
|
private final DjbApistatusIncidentTimelineRepository incidentTimelineRepository;
|
||||||
|
private final ApiStatusDraftTemplate draftTemplate;
|
||||||
|
private final ApiStatusNoticeComposer noticeComposer;
|
||||||
|
|
||||||
|
/** 수동 등록 양식의 제목 키워드. 자동 탐지의 "장애" 와 같은 자리에 들어간다 */
|
||||||
|
private static final String MANUAL_TITLE_KEYWORD = "장애";
|
||||||
|
|
||||||
private String decodeString(String value) {
|
private String decodeString(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (ContainerUtil.get() == ContainerUtil.TOMCAT) {
|
if (ContainerUtil.get() == ContainerUtil.TOMCAT) {
|
||||||
try {
|
try {
|
||||||
return new String(value.getBytes("euc-kr"), StandardCharsets.UTF_8);
|
return new String(value.getBytes("euc-kr"), StandardCharsets.UTF_8);
|
||||||
@@ -70,13 +80,40 @@ public class PortalNoticeManService extends BaseService {
|
|||||||
FileService fileService,
|
FileService fileService,
|
||||||
DjbApistatusIncidentRepository incidentRepository,
|
DjbApistatusIncidentRepository incidentRepository,
|
||||||
DjbApistatusIncidentApiRepository incidentApiRepository,
|
DjbApistatusIncidentApiRepository incidentApiRepository,
|
||||||
DjbApistatusIncidentTimelineRepository incidentTimelineRepository){
|
DjbApistatusIncidentTimelineRepository incidentTimelineRepository,
|
||||||
|
ApiStatusDraftTemplate draftTemplate,
|
||||||
|
ApiStatusNoticeComposer noticeComposer){
|
||||||
this.portalNoticeService = portalNoticeService;
|
this.portalNoticeService = portalNoticeService;
|
||||||
this.portalNoticeUIMapper = portalNoticeUIMapper;
|
this.portalNoticeUIMapper = portalNoticeUIMapper;
|
||||||
this.fileService = fileService;
|
this.fileService = fileService;
|
||||||
this.incidentRepository = incidentRepository;
|
this.incidentRepository = incidentRepository;
|
||||||
this.incidentApiRepository = incidentApiRepository;
|
this.incidentApiRepository = incidentApiRepository;
|
||||||
this.incidentTimelineRepository = incidentTimelineRepository;
|
this.incidentTimelineRepository = incidentTimelineRepository;
|
||||||
|
this.draftTemplate = draftTemplate;
|
||||||
|
this.noticeComposer = noticeComposer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자가 직접 등록할 장애 공지의 제목·본문 양식을 만든다.
|
||||||
|
*
|
||||||
|
* <p>자동 탐지 초안과 같은 문구(PTL_PROPERTY {@code djb.apistatus.draft.*})를 쓴다.
|
||||||
|
* 탐지 사유({@code summary}) 처럼 수동 등록에 없는 값은 치환하지 않고 작성 안내 문구로 대신한다.</p>
|
||||||
|
*
|
||||||
|
* <p>영향 API 는 화면에서 고른 값을 그대로 쓴다 - 선택 팝업이 이미 개발자포탈 게시 API 만
|
||||||
|
* 보여주므로 게시 여부를 다시 판정하지 않는다 (미게시 건수 0).</p>
|
||||||
|
*/
|
||||||
|
@Transactional(transactionManager = "transactionManagerForEMS", readOnly = true)
|
||||||
|
public NoticeDraftTemplateUI buildDraftTemplate(List<IncidentAffectedApiUI> affectedApis) {
|
||||||
|
List<String> labels = affectedApis == null ? Collections.emptyList()
|
||||||
|
: affectedApis.stream()
|
||||||
|
.filter(api -> api != null && StringUtils.isNotBlank(api.getApiId()))
|
||||||
|
.map(api -> StringUtils.defaultIfBlank(api.getApiName(), api.getApiId()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
String subject = noticeComposer.buildManualTitle(MANUAL_TITLE_KEYWORD, labels);
|
||||||
|
String detail = noticeComposer.buildBody(
|
||||||
|
draftTemplate.text(ApiStatusDraftTemplate.BODY_LEAD_MANUAL), labels, 0, true);
|
||||||
|
return new NoticeDraftTemplateUI(subject, detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isIncidentKind(String noticeType) {
|
private static boolean isIncidentKind(String noticeType) {
|
||||||
@@ -399,7 +436,8 @@ public class PortalNoticeManService extends BaseService {
|
|||||||
MultipartFile file = portalNoticeUI.getFiles();
|
MultipartFile file = portalNoticeUI.getFiles();
|
||||||
|
|
||||||
if (file != null && !file.isEmpty()) { // 새로운 파일이 업로드된 경우에만 처리
|
if (file != null && !file.isEmpty()) { // 새로운 파일이 업로드된 경우에만 처리
|
||||||
String decodedFileName = decodeString(portalNoticeUI.getFileName());
|
String fileName = StringUtils.defaultIfBlank(portalNoticeUI.getFileName(), file.getOriginalFilename());
|
||||||
|
String decodedFileName = decodeString(fileName);
|
||||||
FileTypeContext.setFileType("notice");
|
FileTypeContext.setFileType("notice");
|
||||||
FileInfo fileInfo = fileService.createOrUpdateSingleFile(portalNotice.getFileId(), file, decodedFileName, true);
|
FileInfo fileInfo = fileService.createOrUpdateSingleFile(portalNotice.getFileId(), file, decodedFileName, true);
|
||||||
|
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ public class UmsDispatchService {
|
|||||||
try {
|
try {
|
||||||
transactionTemplate.execute(status -> {
|
transactionTemplate.execute(status -> {
|
||||||
try {
|
try {
|
||||||
|
// 상태(SENT/FAILED) 및 RESPONSE_DATA 저장은 umsService.send() 내부에서 처리한다.
|
||||||
umsService.send(message);
|
umsService.send(message);
|
||||||
updateMessageStatus(message, "SENT");
|
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// checked exception을 RuntimeException으로 감싸 전파 → TransactionTemplate이 rollback 후 re-throw
|
// checked exception을 RuntimeException으로 감싸 전파 → TransactionTemplate이 rollback 후 re-throw
|
||||||
@@ -159,7 +159,8 @@ public class UmsDispatchService {
|
|||||||
log.error("Failed to process message: {} - Error: {}", message.getId(), e.getMessage(), e);
|
log.error("Failed to process message: {} - Error: {}", message.getId(), e.getMessage(), e);
|
||||||
try {
|
try {
|
||||||
transactionTemplate.execute(s -> {
|
transactionTemplate.execute(s -> {
|
||||||
updateMessageStatus(message, "FAILED");
|
// 응답 자체를 받지 못한 경우(네트워크/타임아웃 등) 예외 메시지를 RESPONSE_DATA 에 남긴다.
|
||||||
|
updateMessageStatus(message, "FAILED", toResponseData(e));
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -171,10 +172,19 @@ public class UmsDispatchService {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void updateMessageStatus(MessageRequest message, String status) {
|
public void updateMessageStatus(MessageRequest message, String status, String responseData) {
|
||||||
message.setRequestStatus(status);
|
message.setRequestStatus(status);
|
||||||
message.setSentDate(LocalDateTime.now());
|
message.setSentDate(LocalDateTime.now());
|
||||||
|
if (responseData != null) {
|
||||||
|
message.setResponseData(responseData);
|
||||||
|
}
|
||||||
entityManager.merge(message);
|
entityManager.merge(message);
|
||||||
entityManager.flush();
|
entityManager.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 예외를 RESPONSE_DATA 컬럼에 저장할 문자열로 변환한다. (RuntimeException 래퍼는 원인 예외로 풀어서 사용) */
|
||||||
|
private static String toResponseData(Throwable t) {
|
||||||
|
Throwable root = (t instanceof RuntimeException && t.getCause() != null) ? t.getCause() : t;
|
||||||
|
return String.valueOf(root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.eactive.eai.rms.onl.loader.controller;
|
package com.eactive.eai.rms.onl.loader.controller;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -1451,77 +1451,92 @@ public class LoaderController implements InterceptorSkipController {
|
|||||||
*/
|
*/
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
@RequestMapping( params ={"cmd=interface","control=downloadfull"})
|
@RequestMapping( params ={"cmd=interface","control=downloadfull"})
|
||||||
public ModelAndView interfaceDownLoadFully(String serviceType, String interfaceId) throws Exception {
|
public ModelAndView interfaceDownLoadFully(String serviceType, String interfaceId) {
|
||||||
DataSourceType dt = DataSourceTypeManager.getDataSourceType(serviceType);
|
DataSourceType dt = DataSourceTypeManager.getDataSourceType(serviceType);
|
||||||
DataSourceContextHolder.setDataSourceType(dt);
|
DataSourceContextHolder.setDataSourceType(dt);
|
||||||
|
|
||||||
// 1. interface 데이터 다운로드
|
String json = "";
|
||||||
InterfaceDeployPack interfaceDeployPack = new InterfaceDeployPack();
|
try {
|
||||||
|
// 1. interface 데이터 다운로드
|
||||||
|
InterfaceDeployPack interfaceDeployPack = new InterfaceDeployPack();
|
||||||
|
|
||||||
EAIMessageDeploy eaiMessageDeploy = interfaceDeployService.selectDeploy(interfaceId);
|
EAIMessageDeploy eaiMessageDeploy = interfaceDeployService.selectDeploy(interfaceId);
|
||||||
eaiMessageDeploy.setEailastamndyms(null);
|
eaiMessageDeploy.setEailastamndyms(null);
|
||||||
|
|
||||||
interfaceDeployPack.setEaiMessageDeploy(eaiMessageDeploy);
|
interfaceDeployPack.setEaiMessageDeploy(eaiMessageDeploy);
|
||||||
|
|
||||||
for(StandardMessageInfoDeploy standardMessageInfoDeploy : eaiMessageDeploy.getStandardMessageInfoDeploys()){
|
for(StandardMessageInfoDeploy standardMessageInfoDeploy : eaiMessageDeploy.getStandardMessageInfoDeploys()){
|
||||||
standardMessageInfoDeploy.setEailastamndyms(null);
|
standardMessageInfoDeploy.setEailastamndyms(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ServiceMessageDeploy serviceMessageDeploy : eaiMessageDeploy.getServiceMessages()){
|
for (ServiceMessageDeploy serviceMessageDeploy : eaiMessageDeploy.getServiceMessages()){
|
||||||
String[] transformNames = {serviceMessageDeploy.getChngmsgidname(), serviceMessageDeploy.getBascrspnschngmsgidname()};
|
String[] transformNames = {serviceMessageDeploy.getChngmsgidname(), serviceMessageDeploy.getBascrspnschngmsgidname()};
|
||||||
// 2. Layout 데이터 생성
|
// 2. Layout 데이터 생성
|
||||||
for(String transformName: transformNames){
|
for(String transformName: transformNames){
|
||||||
if(StringUtils.isBlank(transformName)){
|
if(StringUtils.isBlank(transformName)){
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(TransformSourceResultUI transformSourceResultUI : transform2Service.selectTransformSourceResult(transformName)){
|
|
||||||
String loutName = transformSourceResultUI.getLoutName();
|
|
||||||
if(StringUtils.isBlank(loutName)){
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
LayoutDeploy layoutDeploy = layoutDeployService.selectDeploy(loutName);
|
|
||||||
layoutDeploy.setLastamndhms(null);
|
for(TransformSourceResultUI transformSourceResultUI : transform2Service.selectTransformSourceResult(transformName)){
|
||||||
interfaceDeployPack.getLayoutDeployList().add(layoutDeploy);
|
String loutName = transformSourceResultUI.getLoutName();
|
||||||
|
if(StringUtils.isBlank(loutName)){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LayoutDeploy layoutDeploy = layoutDeployService.selectDeploy(loutName);
|
||||||
|
layoutDeploy.setLastamndhms(null);
|
||||||
|
interfaceDeployPack.getLayoutDeployList().add(layoutDeploy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Transform 데이터 생성
|
||||||
|
TransformDeploy transformDeploy = transformDeployService.selectDeploy(transformName);
|
||||||
|
transformDeploy.setLastamndhms(null);
|
||||||
|
interfaceDeployPack.getTransformDeployList().add(transformDeploy);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Transform 데이터 생성
|
|
||||||
TransformDeploy transformDeploy = transformDeployService.selectDeploy(transformName);
|
|
||||||
transformDeploy.setLastamndhms(null);
|
|
||||||
interfaceDeployPack.getTransformDeployList().add(transformDeploy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. API 스펙 정보(ptl_api_spec_info) 추가 (감사(audit) 컬럼은 제외)
|
||||||
|
Optional<ApiSpecInfo> optSpecInfo = apiSpecInfoService.findById(interfaceId);
|
||||||
|
if (optSpecInfo.isPresent()) {
|
||||||
|
ApiSpecInfoUI specInfo = apiSpecUIMapper.mapToUI(optSpecInfo.get());
|
||||||
|
specInfo.setCreatedBy(null);
|
||||||
|
specInfo.setCreatedDate(null);
|
||||||
|
specInfo.setLastModifiedBy(null);
|
||||||
|
specInfo.setLastModifiedDate(null);
|
||||||
|
interfaceDeployPack.setSpecInfo(specInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
objectMapper.registerModule(new JavaTimeModule());
|
||||||
|
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||||
|
ObjectWriter writer = objectMapper.writerWithDefaultPrettyPrinter();
|
||||||
|
String bizCode = eaiMessageDeploy.getEaibzwkdstcd();
|
||||||
|
String ioMapPath = monitoringContext.getStringProperty(MonitoringContext.IOMAP_DOWNLOAD_PATH);
|
||||||
|
String fileDir = FilenameUtils.concat(ioMapPath, bizCode);
|
||||||
|
String fileFullPath = FilenameUtils.concat(fileDir, interfaceId+".json");
|
||||||
|
|
||||||
|
FileWriteUtil.makeDirWithGroupPermission(fileDir);
|
||||||
|
File file = new File(fileFullPath);
|
||||||
|
json = writer.writeValueAsString(interfaceDeployPack);
|
||||||
|
FileUtils.writeStringToFile(file, json, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// TSEAITR10 배포 이력 저장 (SERVICENAME=eCams, COMMAND=downloadfull, LOUTNAME=interfaceId, RECVDATA=json)
|
||||||
|
loaderService.insertDeploySyncLog(dt, "downloadfull", interfaceId, json);
|
||||||
|
|
||||||
|
Map<String,String> result = new HashMap<>();
|
||||||
|
result.put("status", "success");
|
||||||
|
result.put("message", fileFullPath+" write done.");
|
||||||
|
|
||||||
|
return new ModelAndView(resultView, "result", result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("interface downloadfull error - interfaceId=" + interfaceId, e);
|
||||||
|
loaderService.insertDeployFailLog(dt, "downloadfull", interfaceId, json, e.toString());
|
||||||
|
|
||||||
|
Map<String,String> result = new HashMap<>();
|
||||||
|
result.put("status", "fail");
|
||||||
|
result.put("message", e.toString());
|
||||||
|
|
||||||
|
return new ModelAndView(resultView, "result", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. API 스펙 정보(ptl_api_spec_info) 추가 (감사(audit) 컬럼은 제외)
|
|
||||||
Optional<ApiSpecInfo> optSpecInfo = apiSpecInfoService.findById(interfaceId);
|
|
||||||
if (optSpecInfo.isPresent()) {
|
|
||||||
ApiSpecInfoUI specInfo = apiSpecUIMapper.mapToUI(optSpecInfo.get());
|
|
||||||
specInfo.setCreatedBy(null);
|
|
||||||
specInfo.setCreatedDate(null);
|
|
||||||
specInfo.setLastModifiedBy(null);
|
|
||||||
specInfo.setLastModifiedDate(null);
|
|
||||||
interfaceDeployPack.setSpecInfo(specInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
objectMapper.registerModule(new JavaTimeModule());
|
|
||||||
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
|
||||||
ObjectWriter writer = objectMapper.writerWithDefaultPrettyPrinter();
|
|
||||||
String bizCode = eaiMessageDeploy.getEaibzwkdstcd();
|
|
||||||
String ioMapPath = monitoringContext.getStringProperty(MonitoringContext.IOMAP_DOWNLOAD_PATH);
|
|
||||||
String fileDir = FilenameUtils.concat(ioMapPath, bizCode);
|
|
||||||
String fileFullPath = FilenameUtils.concat(fileDir, interfaceId+".json");
|
|
||||||
|
|
||||||
FileWriteUtil.makeDirWithGroupPermission(fileDir);
|
|
||||||
File file = new File(fileFullPath);
|
|
||||||
writer.writeValue(file, interfaceDeployPack);
|
|
||||||
|
|
||||||
Map<String,String> result = new HashMap<>();
|
|
||||||
String status = "success";
|
|
||||||
result.put("status", status);
|
|
||||||
result.put("message", fileFullPath+" write done.");
|
|
||||||
|
|
||||||
return new ModelAndView(resultView, "result", result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1533,57 +1548,73 @@ public class LoaderController implements InterceptorSkipController {
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@RequestMapping( params ={"cmd=interface","control=uploadfull"})
|
@RequestMapping( params ={"cmd=interface","control=uploadfull"})
|
||||||
public ModelAndView interfaceLoadFully(String serviceType, String filePath) throws Exception {
|
public ModelAndView interfaceLoadFully(String serviceType, String filePath) {
|
||||||
DataSourceType dt = DataSourceTypeManager.getDataSourceType(serviceType);
|
DataSourceType dt = DataSourceTypeManager.getDataSourceType(serviceType);
|
||||||
DataSourceContextHolder.setDataSourceType(dt);
|
DataSourceContextHolder.setDataSourceType(dt);
|
||||||
|
|
||||||
String ioMapPath = monitoringContext.getStringProperty(MonitoringContext.IOMAP_UPLOAD_PATH);
|
String json = "";
|
||||||
String fileFullPath ;
|
String loutName = filePath;
|
||||||
if(StringUtils.isBlank(ioMapPath)) {
|
try {
|
||||||
fileFullPath = filePath;
|
String ioMapPath = monitoringContext.getStringProperty(MonitoringContext.IOMAP_UPLOAD_PATH);
|
||||||
} else {
|
String fileFullPath ;
|
||||||
fileFullPath = FilenameUtils.concat(ioMapPath, filePath);
|
if(StringUtils.isBlank(ioMapPath)) {
|
||||||
}
|
fileFullPath = filePath;
|
||||||
|
|
||||||
File file = new File(fileFullPath);
|
|
||||||
InputStream inputStream = new FileInputStream(file);
|
|
||||||
// ObjectMapper 생성 및 설정
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
objectMapper.registerModule(new JavaTimeModule());
|
|
||||||
InterfaceDeployPack interfaceDeployPack = objectMapper.readValue(inputStream, InterfaceDeployPack.class);
|
|
||||||
|
|
||||||
EAIMessageDeploy eaiMessageDeploy = interfaceDeployPack.getEaiMessageDeploy();
|
|
||||||
|
|
||||||
interfaceDeployService.saveDeploy(eaiMessageDeploy);
|
|
||||||
agentUtilService.broadcast(new CommonCommand("com.ext.eai.agent.stdmessage.ReloadSTDMessageCommand", "ALL"));
|
|
||||||
agentUtilService.broadcast(new CommonCommand("com.eactive.eai.agent.eaimessage.ReloadEAIMessageCommand", eaiMessageDeploy.getEaisvcname()));
|
|
||||||
|
|
||||||
for(LayoutDeploy layoutDeploy: interfaceDeployPack.getLayoutDeployList()){
|
|
||||||
layoutDeployService.saveDeploy(layoutDeploy);
|
|
||||||
agentUtilService.broadcast(new CommonCommand( "com.eactive.eai.agent.transformer.ReloadLayoutCommand", layoutDeploy.getLoutname()));
|
|
||||||
}
|
|
||||||
|
|
||||||
for(TransformDeploy transformDeploy : interfaceDeployPack.getTransformDeployList()){
|
|
||||||
transformDeployService.saveDeploy(transformDeploy);
|
|
||||||
agentUtilService.broadcast(new CommonCommand( "com.eactive.eai.agent.transformer.ReloadTransformCommand", transformDeploy.getCnvsnname()));
|
|
||||||
}
|
|
||||||
|
|
||||||
ApiSpecInfoUI specInfoUI = interfaceDeployPack.getSpecInfo();
|
|
||||||
if (specInfoUI != null && StringUtils.isNotBlank(specInfoUI.getApiId())) {
|
|
||||||
ApiSpecInfo specInfo = apiSpecUIMapper.mapToEntity(specInfoUI);
|
|
||||||
if (apiSpecInfoService.findById(specInfo.getApiId()).isPresent()) {
|
|
||||||
apiSpecInfoService.updateById(specInfo.getApiId(), specInfo);
|
|
||||||
} else {
|
} else {
|
||||||
apiSpecInfoService.create(specInfo);
|
fileFullPath = FilenameUtils.concat(ioMapPath, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File file = new File(fileFullPath);
|
||||||
|
json = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
|
||||||
|
// ObjectMapper 생성 및 설정
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
objectMapper.registerModule(new JavaTimeModule());
|
||||||
|
InterfaceDeployPack interfaceDeployPack = objectMapper.readValue(json, InterfaceDeployPack.class);
|
||||||
|
|
||||||
|
EAIMessageDeploy eaiMessageDeploy = interfaceDeployPack.getEaiMessageDeploy();
|
||||||
|
loutName = eaiMessageDeploy.getEaisvcname();
|
||||||
|
|
||||||
|
interfaceDeployService.saveDeploy(eaiMessageDeploy);
|
||||||
|
agentUtilService.broadcast(new CommonCommand("com.ext.eai.agent.stdmessage.ReloadSTDMessageCommand", "ALL"));
|
||||||
|
agentUtilService.broadcast(new CommonCommand("com.eactive.eai.agent.eaimessage.ReloadEAIMessageCommand", eaiMessageDeploy.getEaisvcname()));
|
||||||
|
|
||||||
|
for(LayoutDeploy layoutDeploy: interfaceDeployPack.getLayoutDeployList()){
|
||||||
|
layoutDeployService.saveDeploy(layoutDeploy);
|
||||||
|
agentUtilService.broadcast(new CommonCommand( "com.eactive.eai.agent.transformer.ReloadLayoutCommand", layoutDeploy.getLoutname()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(TransformDeploy transformDeploy : interfaceDeployPack.getTransformDeployList()){
|
||||||
|
transformDeployService.saveDeploy(transformDeploy);
|
||||||
|
agentUtilService.broadcast(new CommonCommand( "com.eactive.eai.agent.transformer.ReloadTransformCommand", transformDeploy.getCnvsnname()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiSpecInfoUI specInfoUI = interfaceDeployPack.getSpecInfo();
|
||||||
|
if (specInfoUI != null && StringUtils.isNotBlank(specInfoUI.getApiId())) {
|
||||||
|
ApiSpecInfo specInfo = apiSpecUIMapper.mapToEntity(specInfoUI);
|
||||||
|
if (apiSpecInfoService.findById(specInfo.getApiId()).isPresent()) {
|
||||||
|
apiSpecInfoService.updateById(specInfo.getApiId(), specInfo);
|
||||||
|
} else {
|
||||||
|
apiSpecInfoService.create(specInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TSEAITR10 배포 이력 저장 (SERVICENAME=eCams, COMMAND=uploadfull, LOUTNAME=eaisvcname, RECVDATA=json)
|
||||||
|
loaderService.insertDeploySyncLog(dt, "uploadfull", loutName, json);
|
||||||
|
|
||||||
|
Map<String,String> result = new HashMap<String,String>();
|
||||||
|
result.put("status", "success");
|
||||||
|
result.put("message", fileFullPath+" deploy done.");
|
||||||
|
|
||||||
|
return new ModelAndView(resultView, "result", result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("interface uploadfull error - filePath=" + filePath, e);
|
||||||
|
loaderService.insertDeployFailLog(dt, "uploadfull", loutName, json, e.toString());
|
||||||
|
|
||||||
|
Map<String,String> result = new HashMap<String,String>();
|
||||||
|
result.put("status", "fail");
|
||||||
|
result.put("message", e.toString());
|
||||||
|
|
||||||
|
return new ModelAndView(resultView, "result", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String,String> result = new HashMap<String,String>();
|
|
||||||
String status = "success";
|
|
||||||
result.put("status", status);
|
|
||||||
result.put("message", fileFullPath+" deploy done.");
|
|
||||||
|
|
||||||
return new ModelAndView(resultView, "result", result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,9 @@ public interface LoaderDao {
|
|||||||
public List<LinkedHashMap> selectTableDataForListValue( DataSourceType dataSourceType, String tableName, String key1, List<String> param1 ) ;
|
public List<LinkedHashMap> selectTableDataForListValue( DataSourceType dataSourceType, String tableName, String key1, List<String> param1 ) ;
|
||||||
|
|
||||||
public List<LinkedHashMap> selectSnaAdpApList( DataSourceType dataSourceType, String param1 ) ;
|
public List<LinkedHashMap> selectSnaAdpApList( DataSourceType dataSourceType, String param1 ) ;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TSEAITR10(배포/동기화 이력)에 로그 1건을 저장한다.
|
||||||
|
*/
|
||||||
|
public int insertSyncLog( DataSourceType dt, HashMap<String, String> param ) ;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,11 @@ public class LoaderDaoImpl extends SqlMapClientTemplateDao implements LoaderDao
|
|||||||
// }
|
// }
|
||||||
// }.execute();
|
// }.execute();
|
||||||
}
|
}
|
||||||
|
public int insertSyncLog( DataSourceType dt, HashMap<String, String> param ) {
|
||||||
|
param.put("schemaId", dt.getSchema());
|
||||||
|
return this.template.update("Layout.insertLayoutSyncLog", param);
|
||||||
|
}
|
||||||
|
|
||||||
public List<LinkedHashMap> selectSnaAdpApList( DataSourceType dataSourceType, String param1 ) {
|
public List<LinkedHashMap> selectSnaAdpApList( DataSourceType dataSourceType, String param1 ) {
|
||||||
final HashMap param = new HashMap();
|
final HashMap param = new HashMap();
|
||||||
param.put("param1", param1);
|
param.put("param1", param1);
|
||||||
|
|||||||
@@ -158,4 +158,14 @@ public interface TransactionLoaderService{
|
|||||||
public HashMap syncResult(Command command ,String fileName ) throws Exception;
|
public HashMap syncResult(Command command ,String fileName ) throws Exception;
|
||||||
|
|
||||||
public void appendToListfile(String bizCode, String interfaceId) throws Exception ;
|
public void appendToListfile(String bizCode, String interfaceId) throws Exception ;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* downloadfull / uploadfull 성공 시 TSEAITR10 에 배포 이력을 저장한다. (PRCSSRSLT=S)
|
||||||
|
*/
|
||||||
|
public void insertDeploySyncLog(DataSourceType dt, String command, String interfaceId, String recvData) ;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* downloadfull / uploadfull 실패 시 TSEAITR10 에 실패 이력을 저장한다. (PRCSSRSLT=F, PRCSSRSLTCMNT=errMsg)
|
||||||
|
*/
|
||||||
|
public void insertDeployFailLog(DataSourceType dt, String command, String interfaceId, String recvData, String errMsg) ;
|
||||||
}
|
}
|
||||||
+30
@@ -2,6 +2,7 @@ package com.eactive.eai.rms.onl.loader.service;
|
|||||||
|
|
||||||
import com.eactive.eai.agent.command.Command;
|
import com.eactive.eai.agent.command.Command;
|
||||||
import com.eactive.eai.agent.command.CommonCommand;
|
import com.eactive.eai.agent.command.CommonCommand;
|
||||||
|
import com.eactive.eai.common.util.UUIDGenerator;
|
||||||
import com.eactive.eai.rms.common.base.BaseService;
|
import com.eactive.eai.rms.common.base.BaseService;
|
||||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||||
import com.eactive.eai.rms.common.converter.ElinkObjectMapper;
|
import com.eactive.eai.rms.common.converter.ElinkObjectMapper;
|
||||||
@@ -1483,4 +1484,33 @@ public class TransactionLoaderServiceImpl extends BaseService implements Transac
|
|||||||
String targetLine = bizCode+"|"+interfaceId+System.lineSeparator();
|
String targetLine = bizCode+"|"+interfaceId+System.lineSeparator();
|
||||||
org.apache.commons.io.FileUtils.writeStringToFile(listFile, targetLine, Charset.defaultCharset(), true);
|
org.apache.commons.io.FileUtils.writeStringToFile(listFile, targetLine, Charset.defaultCharset(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertDeploySyncLog(DataSourceType dt, String command, String interfaceId, String recvData) {
|
||||||
|
insertDeployLog(dt, command, interfaceId, recvData, "S", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertDeployFailLog(DataSourceType dt, String command, String interfaceId, String recvData, String errMsg) {
|
||||||
|
insertDeployLog(dt, command, interfaceId, recvData, "F", errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertDeployLog(DataSourceType dt, String command, String interfaceId, String recvData,
|
||||||
|
String prcssRslt, String prcssRsltCmnt) {
|
||||||
|
try {
|
||||||
|
HashMap<String, String> param = new HashMap<String, String>();
|
||||||
|
param.put("logPrcssSeqno", UUIDGenerator.getUUID());
|
||||||
|
param.put("serviceName", "InterfaceDeploy");
|
||||||
|
param.put("command", command);
|
||||||
|
param.put("loutName", interfaceId);
|
||||||
|
param.put("recvData", recvData == null ? "" : recvData);
|
||||||
|
param.put("recvAmndHMS", DateUtil.getDateTime("yyyyMMddHHmmssSS").substring(0, 16));
|
||||||
|
param.put("prcssRslt", prcssRslt);
|
||||||
|
param.put("prcssRsltCmnt", StringUtils.left(prcssRsltCmnt, 4000)); // PRCSSRSLTCMNT 길이 보호
|
||||||
|
dao.insertSyncLog(dt, param);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 이력 저장 실패가 download/upload 본기능을 중단시키지 않도록 한다.
|
||||||
|
logger.error("insertDeployLog error (command=" + command + ", interfaceId=" + interfaceId + ")", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -162,6 +162,11 @@ public class ClientController extends OnlBaseAnnotationController {
|
|||||||
response.put("createdApiSpecCount", result.getCreatedApiSpecCount());
|
response.put("createdApiSpecCount", result.getCreatedApiSpecCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spec 자동생성 실패(레이아웃/어댑터 미설정 등)로 최소 정보만 생성된 API 목록
|
||||||
|
if (result.getSpecGenFailed() != null && !result.getSpecGenFailed().isEmpty()) {
|
||||||
|
response.put("specGenFailed", result.getSpecGenFailed());
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 에러 처리
|
// 에러 처리
|
||||||
|
|||||||
+14
-135
@@ -1,21 +1,12 @@
|
|||||||
package com.eactive.eai.rms.onl.manage.authserver.client;
|
package com.eactive.eai.rms.onl.manage.authserver.client;
|
||||||
|
|
||||||
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
|
|
||||||
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
|
||||||
import com.eactive.apim.portal.app.entity.Credential;
|
|
||||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||||
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
||||||
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
|
||||||
import com.eactive.eai.rms.common.datasource.DataSourceType;
|
|
||||||
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
|
||||||
import com.eactive.eai.rms.data.entity.onl.apim.portalorg.PortalOrgService;
|
import com.eactive.eai.rms.data.entity.onl.apim.portalorg.PortalOrgService;
|
||||||
import com.eactive.eai.rms.data.entity.onl.eaimsg.EAIMessageService;
|
import com.eactive.eai.rms.data.entity.onl.eaimsg.EAIMessageService;
|
||||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialManService;
|
import com.eactive.eai.rms.onl.apim.approval.credential.PortalCredentialSyncService;
|
||||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialService;
|
|
||||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI;
|
|
||||||
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUI;
|
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUI;
|
||||||
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUIMapper;
|
import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgUIMapper;
|
||||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -28,7 +19,6 @@ import com.eactive.eai.rms.common.base.OnlBaseService;
|
|||||||
import com.eactive.eai.rms.data.entity.onl.authserver.ClientEntityService;
|
import com.eactive.eai.rms.data.entity.onl.authserver.ClientEntityService;
|
||||||
import com.eactive.eai.rms.data.entity.onl.authserver.ClientSearch;
|
import com.eactive.eai.rms.data.entity.onl.authserver.ClientSearch;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -42,28 +32,22 @@ public class ClientManService extends OnlBaseService {
|
|||||||
private ClientUIMapper clientUIMapper;
|
private ClientUIMapper clientUIMapper;
|
||||||
private PortalOrgService portalOrgService;
|
private PortalOrgService portalOrgService;
|
||||||
private PortalOrgUIMapper portalOrgUIMapper;
|
private PortalOrgUIMapper portalOrgUIMapper;
|
||||||
private CredentialManService credentialManService;
|
|
||||||
private CredentialService credentialService;
|
|
||||||
private EAIMessageService eaiMessageService;
|
private EAIMessageService eaiMessageService;
|
||||||
private ApiSpecInfoService apiSpecInfoService;
|
private PortalCredentialSyncService portalCredentialSyncService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ClientManService(ClientEntityService clientEntityService,
|
public ClientManService(ClientEntityService clientEntityService,
|
||||||
ClientUIMapper clientUIMapper,
|
ClientUIMapper clientUIMapper,
|
||||||
PortalOrgService portalOrgService,
|
PortalOrgService portalOrgService,
|
||||||
PortalOrgUIMapper portalOrgUIMapper,
|
PortalOrgUIMapper portalOrgUIMapper,
|
||||||
CredentialManService credentialManService,
|
|
||||||
CredentialService credentialService,
|
|
||||||
EAIMessageService eaiMessageService,
|
EAIMessageService eaiMessageService,
|
||||||
ApiSpecInfoService apiSpecInfoService) {
|
PortalCredentialSyncService portalCredentialSyncService) {
|
||||||
this.clientEntityService = clientEntityService;
|
this.clientEntityService = clientEntityService;
|
||||||
this.clientUIMapper = clientUIMapper;
|
this.clientUIMapper = clientUIMapper;
|
||||||
this.portalOrgService = portalOrgService;
|
this.portalOrgService = portalOrgService;
|
||||||
this.portalOrgUIMapper = portalOrgUIMapper;
|
this.portalOrgUIMapper = portalOrgUIMapper;
|
||||||
this.credentialManService = credentialManService;
|
|
||||||
this.credentialService = credentialService;
|
|
||||||
this.eaiMessageService = eaiMessageService;
|
this.eaiMessageService = eaiMessageService;
|
||||||
this.apiSpecInfoService = apiSpecInfoService;
|
this.portalCredentialSyncService = portalCredentialSyncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -127,6 +111,10 @@ public class ClientManService extends OnlBaseService {
|
|||||||
* AGW 인증관리(ClientEntity)의 정보를 개발자포탈(Credential)에 반영합니다.
|
* AGW 인증관리(ClientEntity)의 정보를 개발자포탈(Credential)에 반영합니다.
|
||||||
* 포탈에 없으면 신규 생성(INSERT), 있으면 업데이트(UPDATE)합니다.
|
* 포탈에 없으면 신규 생성(INSERT), 있으면 업데이트(UPDATE)합니다.
|
||||||
*
|
*
|
||||||
|
* 검증(2,3단계)은 APIGW 스키마에서 이 메서드의 트랜잭션 안에서 수행하고, 실패 시 포탈은 전혀 건드리지
|
||||||
|
* 않는다. 실제 포탈 쓰기(Credential/ApiSpecInfo)는 {@link PortalCredentialSyncService}
|
||||||
|
* (별도 EMS 트랜잭션)에 위임한다.
|
||||||
|
*
|
||||||
* @param clientId 클라이언트 ID
|
* @param clientId 클라이언트 ID
|
||||||
* @return 반영 결과 (INSERTED, UPDATED, SKIPPED_NO_ORG, SKIPPED_INVALID_API, ERROR)
|
* @return 반영 결과 (INSERTED, UPDATED, SKIPPED_NO_ORG, SKIPPED_INVALID_API, ERROR)
|
||||||
*/
|
*/
|
||||||
@@ -181,73 +169,20 @@ public class ClientManService extends OnlBaseService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. MONITORING 스키마로 전환
|
// 4. 포탈 스키마에 반영 (별도 EMS 트랜잭션)
|
||||||
DataSourceType monitoringType = DataSourceTypeManager.getDataSourceType("MONITORING");
|
PortalCredentialSyncService.Outcome outcome = portalCredentialSyncService.apply(clientEntity);
|
||||||
DataSourceContextHolder.setDataSourceType(monitoringType);
|
|
||||||
|
|
||||||
// 5. PTL_API_SPEC_INFO에 없는 API 자동 생성
|
|
||||||
List<String> createdApiSpecs = new ArrayList<>();
|
|
||||||
if (clientEntity.getApiList() != null && !clientEntity.getApiList().isEmpty()) {
|
|
||||||
for (EAIMessageEntity apiEntity : clientEntity.getApiList()) {
|
|
||||||
String apiId = apiEntity.getEaisvcname();
|
|
||||||
if (!apiSpecInfoService.findById(apiId).isPresent()) {
|
|
||||||
// PTL_API_SPEC_INFO에 없으면 자동 생성
|
|
||||||
ApiSpecInfo newApiSpec = new ApiSpecInfo();
|
|
||||||
newApiSpec.setApiId(apiId);
|
|
||||||
newApiSpec.setApiName(StringUtils.isNotBlank(apiEntity.getEaisvcdesc())
|
|
||||||
? apiEntity.getEaisvcdesc() : apiId);
|
|
||||||
newApiSpec.setApiSimpleDescription(apiEntity.getEaisvcdesc());
|
|
||||||
newApiSpec.setDisplayYn("Y"); // 기본값: 공개
|
|
||||||
apiSpecInfoService.create(newApiSpec);
|
|
||||||
createdApiSpecs.add(apiId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 기존 Credential 확인
|
|
||||||
Credential existingCredential = credentialService.findByClientidAndOrgid(clientId, orgId);
|
|
||||||
|
|
||||||
// 7. 데이터 변환 (ClientEntity → CredentialUI)
|
|
||||||
CredentialUI credentialUI = convertToCredentialUI(clientEntity);
|
|
||||||
|
|
||||||
SyncResult.SyncAction action;
|
|
||||||
if (existingCredential != null) {
|
|
||||||
// 업데이트: 기존 값 유지해야 할 필드 처리
|
|
||||||
Credential existing = existingCredential;
|
|
||||||
if (StringUtils.isNotBlank(existing.getAppDescription())) {
|
|
||||||
credentialUI.setAppDescription(existing.getAppDescription());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(existing.getAppIconFileId())) {
|
|
||||||
credentialUI.setAppIconFileId(existing.getAppIconFileId());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(existing.getServer())) {
|
|
||||||
credentialUI.setServer(existing.getServer());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 포탈 스키마에 업데이트
|
|
||||||
credentialManService.update(credentialUI);
|
|
||||||
action = SyncResult.SyncAction.UPDATED;
|
|
||||||
} else {
|
|
||||||
// 신규 생성: 기본값 설정
|
|
||||||
if (StringUtils.isBlank(credentialUI.getAppDescription())) {
|
|
||||||
credentialUI.setAppDescription("APIGW에서 반영된 인증정보");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 포탈 스키마에 신규 생성
|
|
||||||
credentialManService.insert(credentialUI);
|
|
||||||
action = SyncResult.SyncAction.INSERTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SyncResult.builder()
|
return SyncResult.builder()
|
||||||
.success(true)
|
.success(true)
|
||||||
.action(action)
|
.action(outcome.isInserted() ? SyncResult.SyncAction.INSERTED : SyncResult.SyncAction.UPDATED)
|
||||||
.message("포탈 정보 반영 완료")
|
.message("포탈 정보 반영 완료")
|
||||||
.targetApis(requestedApiIds)
|
.targetApis(requestedApiIds)
|
||||||
.targetApiCount(requestedApiIds.size())
|
.targetApiCount(requestedApiIds.size())
|
||||||
.syncedApis(requestedApiIds)
|
.syncedApis(requestedApiIds)
|
||||||
.syncedApiCount(requestedApiIds.size())
|
.syncedApiCount(requestedApiIds.size())
|
||||||
.createdApiSpecs(createdApiSpecs)
|
.createdApiSpecs(outcome.getCreatedApiSpecs())
|
||||||
.createdApiSpecCount(createdApiSpecs.size())
|
.createdApiSpecCount(outcome.getCreatedApiSpecs().size())
|
||||||
|
.specGenFailed(outcome.getSpecGenFailed())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -259,60 +194,4 @@ public class ClientManService extends OnlBaseService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ClientEntity를 CredentialUI로 변환합니다.
|
|
||||||
*
|
|
||||||
* @param clientEntity APIGW 클라이언트 엔티티
|
|
||||||
* @return CredentialUI 포탈 인증정보 UI 객체
|
|
||||||
*/
|
|
||||||
private CredentialUI convertToCredentialUI(ClientEntity clientEntity) {
|
|
||||||
CredentialUI credentialUI = new CredentialUI();
|
|
||||||
|
|
||||||
// 기본 인증정보
|
|
||||||
credentialUI.setClientid(clientEntity.getClientid());
|
|
||||||
credentialUI.setClientname(clientEntity.getClientname());
|
|
||||||
credentialUI.setClientsecret(clientEntity.getClientsecret());
|
|
||||||
|
|
||||||
// OAuth 설정
|
|
||||||
credentialUI.setScope(clientEntity.getScope());
|
|
||||||
credentialUI.setGranttypes(clientEntity.getGranttypes());
|
|
||||||
credentialUI.setAccesstokenvalidityseconds(clientEntity.getAccesstokenvalidityseconds());
|
|
||||||
credentialUI.setRefreshtokenvalidityseconds(clientEntity.getRefreshtokenvalidityseconds());
|
|
||||||
|
|
||||||
// 보안 설정
|
|
||||||
credentialUI.setAllowedips(clientEntity.getAllowedips());
|
|
||||||
credentialUI.setAuthorities(clientEntity.getAuthorities());
|
|
||||||
credentialUI.setRedirecturi(clientEntity.getRedirecturi());
|
|
||||||
credentialUI.setSecuritykey(clientEntity.getSecuritykey());
|
|
||||||
credentialUI.setAutoapprove(clientEntity.getAutoapprove());
|
|
||||||
credentialUI.setResourceids(clientEntity.getResourceids());
|
|
||||||
|
|
||||||
// 조직 정보
|
|
||||||
credentialUI.setOrgid(clientEntity.getOrgid());
|
|
||||||
credentialUI.setOrgname(clientEntity.getOrgname());
|
|
||||||
|
|
||||||
// 앱 상태
|
|
||||||
credentialUI.setAppstatus(clientEntity.getAppstatus());
|
|
||||||
credentialUI.setDailytokenlimit(clientEntity.getDailytokenlimit());
|
|
||||||
|
|
||||||
// 수정 정보
|
|
||||||
credentialUI.setModifiedby(clientEntity.getModifiedby());
|
|
||||||
if (clientEntity.getModifiedon() != null) {
|
|
||||||
credentialUI.setModifiedon(clientEntity.getModifiedon().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 목록 변환
|
|
||||||
List<ApiSpecInfoUI> apiList = new ArrayList<>();
|
|
||||||
if (clientEntity.getApiList() != null) {
|
|
||||||
for (EAIMessageEntity apiEntity : clientEntity.getApiList()) {
|
|
||||||
ApiSpecInfoUI apiUI = new ApiSpecInfoUI();
|
|
||||||
apiUI.setApiId(apiEntity.getEaisvcname());
|
|
||||||
apiList.add(apiUI);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
credentialUI.setApiList(apiList);
|
|
||||||
|
|
||||||
return credentialUI;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ public class SyncResult {
|
|||||||
*/
|
*/
|
||||||
private int createdApiSpecCount;
|
private int createdApiSpecCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spec 자동생성에 실패해 최소 정보(apiId만)로 대체 생성된 API 목록
|
||||||
|
* (레이아웃/어댑터 정보 부재 등. PTL_API_SPEC_INFO 행 자체는 생성됨 - displayYn='N')
|
||||||
|
*/
|
||||||
|
private List<String> specGenFailed;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 반영 액션 타입
|
* 반영 액션 타입
|
||||||
*/
|
*/
|
||||||
|
|||||||
+6
@@ -7,6 +7,12 @@ public class LayoutSyncHistoryUISearch {
|
|||||||
|
|
||||||
private String searchLoutName;
|
private String searchLoutName;
|
||||||
|
|
||||||
|
/* 서비스명 (부분일치) */
|
||||||
|
private String searchServiceName;
|
||||||
|
|
||||||
|
/* 커맨드 (부분일치) */
|
||||||
|
private String searchCommand;
|
||||||
|
|
||||||
/* 연동시간 기간 검색 (yyyyMMdd, 8자리) */
|
/* 연동시간 기간 검색 (yyyyMMdd, 8자리) */
|
||||||
private String searchStartDate;
|
private String searchStartDate;
|
||||||
private String searchEndDate;
|
private String searchEndDate;
|
||||||
|
|||||||
@@ -135,6 +135,15 @@ public class ApiInterfaceService extends OnlBaseService {
|
|||||||
public Page<ApiInterfaceUI> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable,String sortname, String sortorder) {
|
public Page<ApiInterfaceUI> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable,String sortname, String sortorder) {
|
||||||
|
|
||||||
Page<Tuple> tuples = eaiMessageQueryService.selectList(eaiMessageUISearch, pageable, sortname, sortorder );
|
Page<Tuple> tuples = eaiMessageQueryService.selectList(eaiMessageUISearch, pageable, sortname, sortorder );
|
||||||
|
List<String> apiIds = tuples.getContent().stream()
|
||||||
|
.map(tuple -> tuple.get(QEAIMessageEntity.eAIMessageEntity).getEaisvcname())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<String, String> specStatuses = eaiMessageQueryService.findApiSpecStatuses(apiIds);
|
||||||
|
List<String> publishedApiIds = apiIds.stream()
|
||||||
|
.filter(apiId -> "PUBLISHED".equals(specStatuses.get(apiId)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<String, List<String>> publishedGroupNames = apiGroupService.findPublishedGroupNamesByApiIds(publishedApiIds);
|
||||||
|
|
||||||
return tuples.map(tuple -> {
|
return tuples.map(tuple -> {
|
||||||
EAIMessageEntity eaiMessageEntity = tuple.get(QEAIMessageEntity.eAIMessageEntity);
|
EAIMessageEntity eaiMessageEntity = tuple.get(QEAIMessageEntity.eAIMessageEntity);
|
||||||
@@ -153,6 +162,8 @@ public class ApiInterfaceService extends OnlBaseService {
|
|||||||
apiInterfaceUI.setApiFullPath(apiFullPath);
|
apiInterfaceUI.setApiFullPath(apiFullPath);
|
||||||
apiInterfaceUI.setBzwksvckeyname(bzwksvckeyname);
|
apiInterfaceUI.setBzwksvckeyname(bzwksvckeyname);
|
||||||
apiInterfaceUI.setStatusCode(statusCode);
|
apiInterfaceUI.setStatusCode(statusCode);
|
||||||
|
apiInterfaceUI.setSpecStatus(specStatuses.getOrDefault(eaiMessageEntity.getEaisvcname(), "UNREGISTERED"));
|
||||||
|
apiInterfaceUI.setPublishedGroupNames(publishedGroupNames.getOrDefault(eaiMessageEntity.getEaisvcname(), Collections.emptyList()));
|
||||||
|
|
||||||
return apiInterfaceUI;
|
return apiInterfaceUI;
|
||||||
});
|
});
|
||||||
@@ -164,6 +175,7 @@ public class ApiInterfaceService extends OnlBaseService {
|
|||||||
// 전체 목록수 얻기
|
// 전체 목록수 얻기
|
||||||
HashMap map = new HashMap();
|
HashMap map = new HashMap();
|
||||||
map.put("bizList", voList);
|
map.put("bizList", voList);
|
||||||
|
map.put("apiGroupList", apiGroupService.findGroupOptions());
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package com.eactive.eai.rms.onl.transaction.apim;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutUI;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
|
||||||
|
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레이아웃+어댑터 기반 API Spec(OpenAPI 3.1) 자동생성 규칙(백엔드 단일 소스).
|
||||||
|
*
|
||||||
|
* {@link DjbApiSpecController}(6단계 편집 마법사)와 인증관리 "개발자포탈 정보 반영"
|
||||||
|
* (ClientManService.syncPortalData → PortalCredentialSyncService) 양쪽이 이 서비스를 공유한다.
|
||||||
|
* 규칙 자체는 원래 DjbApiSpecController 의 private 메서드였던 것을 그대로 옮긴 것으로, 동작 변경은 없다.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DjbApiSpecAutoGenService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DjbApiSpecAutoGenService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApiInterfaceService apiInterfaceService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApiSpecManService apiSpecManService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레이아웃/어댑터 정보를 조합해 OpenAPI 3.1 스펙 JSON 을 생성한다.
|
||||||
|
* 규칙: title/tag/summary = API명(eaiSvcDesc), operationId = 인터페이스ID,
|
||||||
|
* servers[0].url 을 path 앞에 붙이고 servers 제거(서버 선택 제거),
|
||||||
|
* 요청/200 응답 예제를 레이아웃 샘플로 임베드.
|
||||||
|
*/
|
||||||
|
public String generateSpecJson(String eaiSvcName) throws Exception {
|
||||||
|
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||||
|
if (apiInterfaceUI == null) {
|
||||||
|
throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
|
||||||
|
}
|
||||||
|
LayoutUI requestLayoutUI = resolveLayout(apiInterfaceUI, true);
|
||||||
|
LayoutUI responseLayoutUI = resolveLayout(apiInterfaceUI, false);
|
||||||
|
return generateSpecJson(eaiSvcName, apiInterfaceUI, requestLayoutUI, responseLayoutUI);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateSpecJson(String eaiSvcName, ApiInterfaceUI apiInterfaceUI,
|
||||||
|
LayoutUI requestLayoutUI, LayoutUI responseLayoutUI) throws Exception {
|
||||||
|
Map<String, String> inboundAdapterSpec = apiInterfaceService.getHttpAdapterInfo(apiInterfaceUI.getFromAdapter());
|
||||||
|
if (inboundAdapterSpec == null) {
|
||||||
|
inboundAdapterSpec = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseSpec = apiSpecManService.generateSwaggerSpec(requestLayoutUI, responseLayoutUI, inboundAdapterSpec, apiInterfaceUI);
|
||||||
|
|
||||||
|
String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
|
||||||
|
String contentType = StringUtils.isNotBlank(apiInterfaceUI.getRestContentType()) ? apiInterfaceUI.getRestContentType() : "application/json";
|
||||||
|
return applyAutoRules(baseSpec, apiName, eaiSvcName, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 자동생성 결과를 저장 가능한 {@link ApiSpecInfoUI} 로 조립한다.
|
||||||
|
* displayYn 은 호출자가 정한다(비공개로 자동 생성하려면 호출 후 "N" 을 세팅).
|
||||||
|
*/
|
||||||
|
public ApiSpecInfoUI generateApiSpecInfo(String eaiSvcName) throws Exception {
|
||||||
|
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||||
|
if (apiInterfaceUI == null) {
|
||||||
|
throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
|
||||||
|
LayoutUI requestLayoutUI = resolveLayout(apiInterfaceUI, true);
|
||||||
|
LayoutUI responseLayoutUI = resolveLayout(apiInterfaceUI, false);
|
||||||
|
String specJson = generateSpecJson(eaiSvcName, apiInterfaceUI, requestLayoutUI, responseLayoutUI);
|
||||||
|
|
||||||
|
ApiSpecInfoUI ui = new ApiSpecInfoUI();
|
||||||
|
ui.setApiId(eaiSvcName);
|
||||||
|
ui.setApiName(apiName);
|
||||||
|
ui.setApiSimpleDescription(apiInterfaceUI.getEaiSvcDesc());
|
||||||
|
ui.setTestbedSpec(specJson);
|
||||||
|
ui.setApiRequestSpec(apiSpecManService.generateSpecTableFromLayout(requestLayoutUI));
|
||||||
|
ui.setApiResponseSpec(apiSpecManService.generateSpecTableFromLayout(responseLayoutUI));
|
||||||
|
ui.setSampleRequest(apiSpecManService.generateSampleDataFromLayout(requestLayoutUI));
|
||||||
|
ui.setSampleResponse(apiSpecManService.generateSampleDataFromLayout(responseLayoutUI));
|
||||||
|
|
||||||
|
extractFirstOperation(specJson, ui);
|
||||||
|
return ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 생성된 spec 의 첫 path/method/requestBody mediaType 을 추출한다(마법사 프론트와 동일 규칙, app.js 의 _defScalar 대응). */
|
||||||
|
private void extractFirstOperation(String specJson, ApiSpecInfoUI ui) {
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(specJson);
|
||||||
|
JsonNode pathsNode = root.get("paths");
|
||||||
|
if (pathsNode == null || !pathsNode.isObject() || pathsNode.size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Iterator<Map.Entry<String, JsonNode>> pathIt = pathsNode.fields();
|
||||||
|
if (!pathIt.hasNext()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map.Entry<String, JsonNode> pathEntry = pathIt.next();
|
||||||
|
ui.setApiUrl(pathEntry.getKey());
|
||||||
|
|
||||||
|
JsonNode pathItem = pathEntry.getValue();
|
||||||
|
if (pathItem != null && pathItem.isObject() && pathItem.fields().hasNext()) {
|
||||||
|
Map.Entry<String, JsonNode> methodEntry = pathItem.fields().next();
|
||||||
|
ui.setApiMethod(methodEntry.getKey().toUpperCase());
|
||||||
|
|
||||||
|
JsonNode op = methodEntry.getValue();
|
||||||
|
if (op != null && op.isObject()) {
|
||||||
|
JsonNode content = op.path("requestBody").path("content");
|
||||||
|
if (content.isObject() && content.fields().hasNext()) {
|
||||||
|
ui.setApiContentType(content.fields().next().getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("자동생성 spec 에서 apiUrl/apiMethod 추출 실패: " + ui.getApiId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LayoutUI resolveLayout(ApiInterfaceUI apiInterfaceUI, boolean request) throws Exception {
|
||||||
|
String layoutName = request ? apiInterfaceUI.getInboundRequestLayout() : apiInterfaceUI.getInboundResponseLayout();
|
||||||
|
return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 자동생성 규칙 후처리(Jackson 트리 조작). 실패 시 원본 유지. */
|
||||||
|
private String applyAutoRules(String specJson, String apiName, String eaiSvcName, String contentType) {
|
||||||
|
try {
|
||||||
|
JsonNode parsed = objectMapper.readTree(specJson);
|
||||||
|
if (!parsed.isObject()) {
|
||||||
|
return specJson;
|
||||||
|
}
|
||||||
|
ObjectNode root = (ObjectNode) parsed;
|
||||||
|
|
||||||
|
root.put("openapi", "3.1.0"); // OpenAPI 3.1 방출(Swagger UI 5.x oas31 렌더 지원)
|
||||||
|
getOrCreateObject(root, "info").put("title", apiName);
|
||||||
|
|
||||||
|
String tagName = "DJBank"; // 자동생성 태그명 고정
|
||||||
|
ArrayNode tags = objectMapper.createArrayNode();
|
||||||
|
ObjectNode tag = objectMapper.createObjectNode();
|
||||||
|
tag.put("name", tagName);
|
||||||
|
tag.put("description", apiName); // 태그 설명 = 간단설명(API명)
|
||||||
|
tags.add(tag);
|
||||||
|
root.set("tags", tags);
|
||||||
|
|
||||||
|
// 어댑터경로(servers[0].url = getHttpAdapterInfo urlPath)를 path 앞에 baking 하고 servers 제거.
|
||||||
|
// 호스트(gw=djb.gateway.base-url / mock=Mock URL / sample=없음)는 프론트가 응답유형에 따라 서버로 붙인다.
|
||||||
|
String adapterPath = "";
|
||||||
|
JsonNode serversNode = root.get("servers");
|
||||||
|
if (serversNode != null && serversNode.isArray() && serversNode.size() > 0 && serversNode.get(0).get("url") != null) {
|
||||||
|
adapterPath = serversNode.get(0).get("url").asText("");
|
||||||
|
}
|
||||||
|
root.remove("servers");
|
||||||
|
|
||||||
|
JsonNode pathsNode = root.get("paths");
|
||||||
|
if (pathsNode != null && pathsNode.isObject()) {
|
||||||
|
ObjectNode paths = (ObjectNode) pathsNode;
|
||||||
|
List<String> pathKeys = new ArrayList<>();
|
||||||
|
Iterator<String> pit = paths.fieldNames();
|
||||||
|
while (pit.hasNext()) {
|
||||||
|
pathKeys.add(pit.next());
|
||||||
|
}
|
||||||
|
for (String pk : pathKeys) {
|
||||||
|
JsonNode pathItem = paths.get(pk);
|
||||||
|
if (pathItem != null && pathItem.isObject()) {
|
||||||
|
List<String> methods = new ArrayList<>();
|
||||||
|
Iterator<String> mit = pathItem.fieldNames();
|
||||||
|
while (mit.hasNext()) {
|
||||||
|
methods.add(mit.next());
|
||||||
|
}
|
||||||
|
for (String mk : methods) {
|
||||||
|
JsonNode opNode = pathItem.get(mk);
|
||||||
|
if (opNode != null && opNode.isObject()) {
|
||||||
|
ObjectNode op = (ObjectNode) opNode;
|
||||||
|
op.put("operationId", eaiSvcName);
|
||||||
|
op.put("summary", apiName);
|
||||||
|
ArrayNode opTags = objectMapper.createArrayNode();
|
||||||
|
opTags.add(tagName);
|
||||||
|
op.set("tags", opTags);
|
||||||
|
// 예제(요청/응답 본문)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치. 여기선 임베드하지 않음.
|
||||||
|
markGwOnOperation(op);
|
||||||
|
addResponseHeader(op, "200", "Content-Type", contentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String newKey = adapterPath.isEmpty() ? pk
|
||||||
|
: (adapterPath.replaceAll("/+$", "") + (pk.startsWith("/") ? "" : "/") + pk);
|
||||||
|
if (!newKey.equals(pk)) {
|
||||||
|
paths.set(newKey, pathItem);
|
||||||
|
paths.remove(pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objectMapper.writeValueAsString(root);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("applyAutoRules 실패, 원본 spec 유지", e);
|
||||||
|
return specJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GW 유래 스키마 필드에 x-djb-gw 마커 부여(프론트에서 잠금 표시). */
|
||||||
|
private void markGwOnOperation(ObjectNode op) {
|
||||||
|
JsonNode body = op.get("requestBody");
|
||||||
|
if (body != null && body.isObject()) {
|
||||||
|
markGwInContent(body.get("content"));
|
||||||
|
}
|
||||||
|
JsonNode resps = op.get("responses");
|
||||||
|
if (resps != null && resps.isObject()) {
|
||||||
|
List<String> codes = new ArrayList<>();
|
||||||
|
Iterator<String> it = resps.fieldNames();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
codes.add(it.next());
|
||||||
|
}
|
||||||
|
for (String c : codes) {
|
||||||
|
JsonNode r = resps.get(c);
|
||||||
|
if (r != null && r.isObject()) {
|
||||||
|
markGwInContent(r.get("content"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markGwInContent(JsonNode content) {
|
||||||
|
if (content == null || !content.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> mts = new ArrayList<>();
|
||||||
|
Iterator<String> it = content.fieldNames();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
mts.add(it.next());
|
||||||
|
}
|
||||||
|
for (String mt : mts) {
|
||||||
|
JsonNode m = content.get(mt);
|
||||||
|
if (m != null && m.isObject()) {
|
||||||
|
markGwSchema(m.get("schema"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markGwSchema(JsonNode schema) {
|
||||||
|
if (schema == null || !schema.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ObjectNode s = (ObjectNode) schema;
|
||||||
|
JsonNode props = s.get("properties");
|
||||||
|
if (props != null && props.isObject()) {
|
||||||
|
ArrayNode required = (s.get("required") != null && s.get("required").isArray())
|
||||||
|
? (ArrayNode) s.get("required") : objectMapper.createArrayNode();
|
||||||
|
java.util.Set<String> existing = new java.util.HashSet<>();
|
||||||
|
required.forEach(n -> existing.add(n.asText()));
|
||||||
|
List<String> keys = new ArrayList<>();
|
||||||
|
Iterator<String> it = props.fieldNames();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
keys.add(it.next());
|
||||||
|
}
|
||||||
|
for (String k : keys) {
|
||||||
|
JsonNode p = props.get(k);
|
||||||
|
if (p != null && p.isObject()) {
|
||||||
|
((ObjectNode) p).put("x-djb-gw", true);
|
||||||
|
if (!existing.contains(k)) { // GW 필드는 기본 '필수'
|
||||||
|
required.add(k);
|
||||||
|
existing.add(k);
|
||||||
|
}
|
||||||
|
markGwSchema(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (required.size() > 0) {
|
||||||
|
s.set("required", required);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JsonNode items = s.get("items");
|
||||||
|
if (items != null && items.isObject()) {
|
||||||
|
markGwSchema(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 응답 헤더 미리 등록(중복 시 스킵). */
|
||||||
|
private void addResponseHeader(ObjectNode op, String code, String headerName, String example) {
|
||||||
|
JsonNode resps = op.get("responses");
|
||||||
|
if (resps == null || !resps.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonNode r = resps.get(code);
|
||||||
|
if (r == null || !r.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ObjectNode headers = getOrCreateObject((ObjectNode) r, "headers");
|
||||||
|
if (headers.has(headerName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ObjectNode h = objectMapper.createObjectNode();
|
||||||
|
h.put("description", headerName);
|
||||||
|
ObjectNode sc = objectMapper.createObjectNode();
|
||||||
|
sc.put("type", "string");
|
||||||
|
h.set("schema", sc);
|
||||||
|
h.put("example", example);
|
||||||
|
headers.set(headerName, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
||||||
|
JsonNode node = parent.get(field);
|
||||||
|
if (node != null && node.isObject()) {
|
||||||
|
return (ObjectNode) node;
|
||||||
|
}
|
||||||
|
ObjectNode created = objectMapper.createObjectNode();
|
||||||
|
parent.set(field, created);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,13 @@ package com.eactive.eai.rms.onl.transaction.apim;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.fasterxml.jackson.databind.node.TextNode;
|
|
||||||
|
|
||||||
import org.apache.commons.lang.StringEscapeUtils;
|
import org.apache.commons.lang.StringEscapeUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -60,6 +57,9 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DjbApiSpecLayoutMergeService layoutMergeService;
|
private DjbApiSpecLayoutMergeService layoutMergeService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DjbApiSpecAutoGenService autoGenService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private PortalPropertyService portalPropertyService;
|
private PortalPropertyService portalPropertyService;
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
|
|||||||
spec = saved.getTestbedSpec();
|
spec = saved.getTestbedSpec();
|
||||||
source = "saved";
|
source = "saved";
|
||||||
} else {
|
} else {
|
||||||
spec = generateSpec(eaiSvcName);
|
spec = autoGenService.generateSpecJson(eaiSvcName);
|
||||||
source = force ? "regenerated" : "generated";
|
source = force ? "regenerated" : "generated";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,268 +324,6 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
|
|||||||
return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
|
return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 레이아웃 기반 자동 생성 + 자동생성 규칙(백엔드 단일 소스).
|
|
||||||
* 규칙: title/tag/summary = API명(eaiSvcDesc), operationId = 인터페이스ID,
|
|
||||||
* servers[0].url 을 path 앞에 붙이고 servers 제거(서버 선택 제거),
|
|
||||||
* 요청/200 응답 예제를 레이아웃 샘플로 임베드.
|
|
||||||
*/
|
|
||||||
private String generateSpec(String eaiSvcName) throws Exception {
|
|
||||||
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
|
||||||
if (apiInterfaceUI == null) {
|
|
||||||
throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
|
|
||||||
}
|
|
||||||
Map<String, String> inboundAdapterSpec = apiInterfaceService.getHttpAdapterInfo(apiInterfaceUI.getFromAdapter());
|
|
||||||
if (inboundAdapterSpec == null) {
|
|
||||||
inboundAdapterSpec = new HashMap<>();
|
|
||||||
}
|
|
||||||
String requestLayoutName = apiInterfaceUI.getInboundRequestLayout();
|
|
||||||
String responseLayoutName = apiInterfaceUI.getInboundResponseLayout();
|
|
||||||
LayoutUI requestLayoutUI = StringUtils.isNotEmpty(requestLayoutName)
|
|
||||||
? apiSpecManService.selectLayoutUI(requestLayoutName) : null;
|
|
||||||
LayoutUI responseLayoutUI = StringUtils.isNotEmpty(responseLayoutName)
|
|
||||||
? apiSpecManService.selectLayoutUI(responseLayoutName) : null;
|
|
||||||
|
|
||||||
String baseSpec = apiSpecManService.generateSwaggerSpec(requestLayoutUI, responseLayoutUI, inboundAdapterSpec, apiInterfaceUI);
|
|
||||||
|
|
||||||
String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
|
|
||||||
String reqSample = apiSpecManService.generateSampleDataFromLayout(requestLayoutUI);
|
|
||||||
String resSample = apiSpecManService.generateSampleDataFromLayout(responseLayoutUI);
|
|
||||||
String contentType = StringUtils.isNotBlank(apiInterfaceUI.getRestContentType()) ? apiInterfaceUI.getRestContentType() : "application/json";
|
|
||||||
return applyAutoRules(baseSpec, apiName, eaiSvcName, reqSample, resSample, contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 자동생성 규칙 후처리(Jackson 트리 조작). 실패 시 원본 유지. */
|
|
||||||
private String applyAutoRules(String specJson, String apiName, String eaiSvcName, String reqSample, String resSample, String contentType) {
|
|
||||||
try {
|
|
||||||
JsonNode parsed = objectMapper.readTree(specJson);
|
|
||||||
if (!parsed.isObject()) {
|
|
||||||
return specJson;
|
|
||||||
}
|
|
||||||
ObjectNode root = (ObjectNode) parsed;
|
|
||||||
|
|
||||||
root.put("openapi", "3.1.0"); // OpenAPI 3.1 방출(Swagger UI 5.x oas31 렌더 지원)
|
|
||||||
getOrCreateObject(root, "info").put("title", apiName);
|
|
||||||
|
|
||||||
String tagName = "DJBank"; // 자동생성 태그명 고정
|
|
||||||
ArrayNode tags = objectMapper.createArrayNode();
|
|
||||||
ObjectNode tag = objectMapper.createObjectNode();
|
|
||||||
tag.put("name", tagName);
|
|
||||||
tag.put("description", apiName); // 태그 설명 = 간단설명(API명)
|
|
||||||
tags.add(tag);
|
|
||||||
root.set("tags", tags);
|
|
||||||
|
|
||||||
// 어댑터경로(servers[0].url = getHttpAdapterInfo urlPath)를 path 앞에 baking 하고 servers 제거.
|
|
||||||
// 호스트(gw=djb.gateway.base-url / mock=Mock URL / sample=없음)는 프론트가 응답유형에 따라 서버로 붙인다.
|
|
||||||
String adapterPath = "";
|
|
||||||
JsonNode serversNode = root.get("servers");
|
|
||||||
if (serversNode != null && serversNode.isArray() && serversNode.size() > 0 && serversNode.get(0).get("url") != null) {
|
|
||||||
adapterPath = serversNode.get(0).get("url").asText("");
|
|
||||||
}
|
|
||||||
root.remove("servers");
|
|
||||||
|
|
||||||
JsonNode pathsNode = root.get("paths");
|
|
||||||
if (pathsNode != null && pathsNode.isObject()) {
|
|
||||||
ObjectNode paths = (ObjectNode) pathsNode;
|
|
||||||
List<String> pathKeys = new ArrayList<>();
|
|
||||||
Iterator<String> pit = paths.fieldNames();
|
|
||||||
while (pit.hasNext()) {
|
|
||||||
pathKeys.add(pit.next());
|
|
||||||
}
|
|
||||||
for (String pk : pathKeys) {
|
|
||||||
JsonNode pathItem = paths.get(pk);
|
|
||||||
if (pathItem != null && pathItem.isObject()) {
|
|
||||||
List<String> methods = new ArrayList<>();
|
|
||||||
Iterator<String> mit = pathItem.fieldNames();
|
|
||||||
while (mit.hasNext()) {
|
|
||||||
methods.add(mit.next());
|
|
||||||
}
|
|
||||||
for (String mk : methods) {
|
|
||||||
JsonNode opNode = pathItem.get(mk);
|
|
||||||
if (opNode != null && opNode.isObject()) {
|
|
||||||
ObjectNode op = (ObjectNode) opNode;
|
|
||||||
op.put("operationId", eaiSvcName);
|
|
||||||
op.put("summary", apiName);
|
|
||||||
ArrayNode opTags = objectMapper.createArrayNode();
|
|
||||||
opTags.add(tagName);
|
|
||||||
op.set("tags", opTags);
|
|
||||||
// 예제(요청/응답 본문)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치. 여기선 임베드하지 않음.
|
|
||||||
markGwOnOperation(op);
|
|
||||||
addResponseHeader(op, "200", "Content-Type", contentType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String newKey = adapterPath.isEmpty() ? pk
|
|
||||||
: (adapterPath.replaceAll("/+$", "") + (pk.startsWith("/") ? "" : "/") + pk);
|
|
||||||
if (!newKey.equals(pk)) {
|
|
||||||
paths.set(newKey, pathItem);
|
|
||||||
paths.remove(pk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return objectMapper.writeValueAsString(root);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("applyAutoRules 실패, 원본 spec 유지", e);
|
|
||||||
return specJson;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setContentExample(ObjectNode op, String sampleJson) {
|
|
||||||
if (StringUtils.isBlank(sampleJson)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode body = op.get("requestBody");
|
|
||||||
if (body == null || !body.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode content = body.get("content");
|
|
||||||
if (content == null || !content.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Iterator<String> mts = content.fieldNames();
|
|
||||||
if (!mts.hasNext()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode mtNode = content.get(mts.next());
|
|
||||||
if (mtNode != null && mtNode.isObject()) {
|
|
||||||
((ObjectNode) mtNode).set("example", parseJsonOrText(sampleJson));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setResponseExample(ObjectNode op, String code, String sampleJson) {
|
|
||||||
if (StringUtils.isBlank(sampleJson)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode resps = op.get("responses");
|
|
||||||
if (resps == null || !resps.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode r = resps.get(code);
|
|
||||||
if (r == null || !r.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ObjectNode content = getOrCreateObject((ObjectNode) r, "content");
|
|
||||||
ObjectNode appjson = getOrCreateObject(content, "application/json");
|
|
||||||
appjson.set("example", parseJsonOrText(sampleJson));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GW 유래 스키마 필드에 x-djb-gw 마커 부여(프론트에서 잠금 표시). */
|
|
||||||
private void markGwOnOperation(ObjectNode op) {
|
|
||||||
JsonNode body = op.get("requestBody");
|
|
||||||
if (body != null && body.isObject()) {
|
|
||||||
markGwInContent(body.get("content"));
|
|
||||||
}
|
|
||||||
JsonNode resps = op.get("responses");
|
|
||||||
if (resps != null && resps.isObject()) {
|
|
||||||
List<String> codes = new ArrayList<>();
|
|
||||||
Iterator<String> it = resps.fieldNames();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
codes.add(it.next());
|
|
||||||
}
|
|
||||||
for (String c : codes) {
|
|
||||||
JsonNode r = resps.get(c);
|
|
||||||
if (r != null && r.isObject()) {
|
|
||||||
markGwInContent(r.get("content"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void markGwInContent(JsonNode content) {
|
|
||||||
if (content == null || !content.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List<String> mts = new ArrayList<>();
|
|
||||||
Iterator<String> it = content.fieldNames();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
mts.add(it.next());
|
|
||||||
}
|
|
||||||
for (String mt : mts) {
|
|
||||||
JsonNode m = content.get(mt);
|
|
||||||
if (m != null && m.isObject()) {
|
|
||||||
markGwSchema(m.get("schema"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void markGwSchema(JsonNode schema) {
|
|
||||||
if (schema == null || !schema.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ObjectNode s = (ObjectNode) schema;
|
|
||||||
JsonNode props = s.get("properties");
|
|
||||||
if (props != null && props.isObject()) {
|
|
||||||
ArrayNode required = (s.get("required") != null && s.get("required").isArray())
|
|
||||||
? (ArrayNode) s.get("required") : objectMapper.createArrayNode();
|
|
||||||
java.util.Set<String> existing = new java.util.HashSet<>();
|
|
||||||
required.forEach(n -> existing.add(n.asText()));
|
|
||||||
List<String> keys = new ArrayList<>();
|
|
||||||
Iterator<String> it = props.fieldNames();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
keys.add(it.next());
|
|
||||||
}
|
|
||||||
for (String k : keys) {
|
|
||||||
JsonNode p = props.get(k);
|
|
||||||
if (p != null && p.isObject()) {
|
|
||||||
((ObjectNode) p).put("x-djb-gw", true);
|
|
||||||
if (!existing.contains(k)) { // GW 필드는 기본 '필수'
|
|
||||||
required.add(k);
|
|
||||||
existing.add(k);
|
|
||||||
}
|
|
||||||
markGwSchema(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (required.size() > 0) {
|
|
||||||
s.set("required", required);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JsonNode items = s.get("items");
|
|
||||||
if (items != null && items.isObject()) {
|
|
||||||
markGwSchema(items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 응답 헤더 미리 등록(중복 시 스킵). */
|
|
||||||
private void addResponseHeader(ObjectNode op, String code, String headerName, String example) {
|
|
||||||
JsonNode resps = op.get("responses");
|
|
||||||
if (resps == null || !resps.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
JsonNode r = resps.get(code);
|
|
||||||
if (r == null || !r.isObject()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ObjectNode headers = getOrCreateObject((ObjectNode) r, "headers");
|
|
||||||
if (headers.has(headerName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ObjectNode h = objectMapper.createObjectNode();
|
|
||||||
h.put("description", headerName);
|
|
||||||
ObjectNode sc = objectMapper.createObjectNode();
|
|
||||||
sc.put("type", "string");
|
|
||||||
h.set("schema", sc);
|
|
||||||
h.put("example", example);
|
|
||||||
headers.set(headerName, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonNode parseJsonOrText(String s) {
|
|
||||||
try {
|
|
||||||
return objectMapper.readTree(s);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return TextNode.valueOf(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
|
||||||
JsonNode node = parent.get(field);
|
|
||||||
if (node != null && node.isObject()) {
|
|
||||||
return (ObjectNode) node;
|
|
||||||
}
|
|
||||||
ObjectNode created = objectMapper.createObjectNode();
|
|
||||||
parent.set(field, created);
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void putIfNotNull(Map<String, String> map, String key, String value) {
|
private void putIfNotNull(Map<String, String> map, String key, String value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map.put(key, value);
|
map.put(key, value);
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ public class ApiInterfaceUI {
|
|||||||
private String apiFullPath;
|
private String apiFullPath;
|
||||||
private String bzwksvckeyname;
|
private String bzwksvckeyname;
|
||||||
private String statusCode;
|
private String statusCode;
|
||||||
|
private String specStatus;
|
||||||
|
private List<String> publishedGroupNames;
|
||||||
|
|
||||||
//ASYNC-SYNC 용
|
//ASYNC-SYNC 용
|
||||||
private String inboundResponseHttpMethod;
|
private String inboundResponseHttpMethod;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import java.io.IOException;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -18,7 +20,10 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricJpaDAO;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService;
|
||||||
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
||||||
@@ -38,6 +43,7 @@ public class ApiStatsDayController {
|
|||||||
private final ApiStatsDayService service;
|
private final ApiStatsDayService service;
|
||||||
private final ApiStatsUIMapper mapper;
|
private final ApiStatsUIMapper mapper;
|
||||||
private final ApiStatsExcelExportService excelExportService;
|
private final ApiStatsExcelExportService excelExportService;
|
||||||
|
private final ObpGwMetricJpaDAO apiNameDao;
|
||||||
|
|
||||||
@GetMapping(value = "/onl/kjb/statistics/apiStatsDayMan.view")
|
@GetMapping(value = "/onl/kjb/statistics/apiStatsDayMan.view")
|
||||||
public String view() {
|
public String view() {
|
||||||
@@ -66,9 +72,38 @@ public class ApiStatsDayController {
|
|||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.json", params = "cmd=LIST_SUMMARY")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.json", params = "cmd=LIST_SUMMARY")
|
||||||
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
||||||
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
||||||
|
fillApiDesc(page.getContent());
|
||||||
return ResponseEntity.ok(new GridResponse<>(page));
|
return ResponseEntity.ok(new GridResponse<>(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요약 통계 행의 apiName(=API ID)으로 TSEAIHE01 을 조회하여 API 명(EAISVCDESC)을 채운다.
|
||||||
|
* 매칭되는 항목이 없으면 공란으로 둔다.
|
||||||
|
*/
|
||||||
|
private void fillApiDesc(List<ApiStatsUI> rows) {
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> apiIds = rows.stream()
|
||||||
|
.map(ApiStatsUI::getApiName)
|
||||||
|
.filter(id -> id != null && !id.isEmpty())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (apiIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API_STATS_DAY 와 동일한 APIGW 스키마에서 조회
|
||||||
|
DataSourceContextHolder.setDataSourceType(
|
||||||
|
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
|
||||||
|
|
||||||
|
Map<String, String> nameMap = apiNameDao.selectApiNames(apiIds);
|
||||||
|
for (ApiStatsUI row : rows) {
|
||||||
|
row.setApiDesc(nameMap.get(row.getApiName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.json", params = "cmd=EXCEL_EXPORT")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.json", params = "cmd=EXCEL_EXPORT")
|
||||||
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
||||||
log.info("Excel export started - search: {}", search);
|
log.info("Excel export started - search: {}", search);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import java.io.IOException;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -18,7 +20,10 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricJpaDAO;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService;
|
||||||
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
||||||
@@ -38,6 +43,7 @@ public class ApiStatsHourController {
|
|||||||
private final ApiStatsHourService service;
|
private final ApiStatsHourService service;
|
||||||
private final ApiStatsUIMapper mapper;
|
private final ApiStatsUIMapper mapper;
|
||||||
private final ApiStatsExcelExportService excelExportService;
|
private final ApiStatsExcelExportService excelExportService;
|
||||||
|
private final ObpGwMetricJpaDAO apiNameDao;
|
||||||
|
|
||||||
@GetMapping(value = "/onl/kjb/statistics/apiStatsHourMan.view")
|
@GetMapping(value = "/onl/kjb/statistics/apiStatsHourMan.view")
|
||||||
public String view() {
|
public String view() {
|
||||||
@@ -66,9 +72,38 @@ public class ApiStatsHourController {
|
|||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsHourMan.json", params = "cmd=LIST_SUMMARY")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsHourMan.json", params = "cmd=LIST_SUMMARY")
|
||||||
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
||||||
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
||||||
|
fillApiDesc(page.getContent());
|
||||||
return ResponseEntity.ok(new GridResponse<>(page));
|
return ResponseEntity.ok(new GridResponse<>(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요약 통계 행의 apiName(=API ID)으로 TSEAIHE01 을 조회하여 API 명(EAISVCDESC)을 채운다.
|
||||||
|
* 매칭되는 항목이 없으면 공란으로 둔다.
|
||||||
|
*/
|
||||||
|
private void fillApiDesc(List<ApiStatsUI> rows) {
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> apiIds = rows.stream()
|
||||||
|
.map(ApiStatsUI::getApiName)
|
||||||
|
.filter(id -> id != null && !id.isEmpty())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (apiIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API_STATS_HOUR 와 동일한 APIGW 스키마에서 조회
|
||||||
|
DataSourceContextHolder.setDataSourceType(
|
||||||
|
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
|
||||||
|
|
||||||
|
Map<String, String> nameMap = apiNameDao.selectApiNames(apiIds);
|
||||||
|
for (ApiStatsUI row : rows) {
|
||||||
|
row.setApiDesc(nameMap.get(row.getApiName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsHourMan.json", params = "cmd=EXCEL_EXPORT")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsHourMan.json", params = "cmd=EXCEL_EXPORT")
|
||||||
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
||||||
log.info("Excel export started - search: {}", search);
|
log.info("Excel export started - search: {}", search);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import java.io.IOException;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -18,7 +20,10 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricJpaDAO;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinuteService;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinuteService;
|
||||||
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
||||||
@@ -38,6 +43,7 @@ public class ApiStatsMinuteController {
|
|||||||
private final ApiStatsMinuteService service;
|
private final ApiStatsMinuteService service;
|
||||||
private final ApiStatsUIMapper mapper;
|
private final ApiStatsUIMapper mapper;
|
||||||
private final ApiStatsExcelExportService excelExportService;
|
private final ApiStatsExcelExportService excelExportService;
|
||||||
|
private final ObpGwMetricJpaDAO apiNameDao;
|
||||||
|
|
||||||
@GetMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.view")
|
@GetMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.view")
|
||||||
public String view() {
|
public String view() {
|
||||||
@@ -66,9 +72,38 @@ public class ApiStatsMinuteController {
|
|||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.json", params = "cmd=LIST_SUMMARY")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.json", params = "cmd=LIST_SUMMARY")
|
||||||
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
||||||
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
||||||
|
fillApiDesc(page.getContent());
|
||||||
return ResponseEntity.ok(new GridResponse<>(page));
|
return ResponseEntity.ok(new GridResponse<>(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요약 통계 행의 apiName(=API ID)으로 TSEAIHE01 을 조회하여 API 명(EAISVCDESC)을 채운다.
|
||||||
|
* 매칭되는 항목이 없으면 공란으로 둔다.
|
||||||
|
*/
|
||||||
|
private void fillApiDesc(List<ApiStatsUI> rows) {
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> apiIds = rows.stream()
|
||||||
|
.map(ApiStatsUI::getApiName)
|
||||||
|
.filter(id -> id != null && !id.isEmpty())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (apiIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API_STATS_MINUTE 와 동일한 APIGW 스키마에서 조회
|
||||||
|
DataSourceContextHolder.setDataSourceType(
|
||||||
|
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
|
||||||
|
|
||||||
|
Map<String, String> nameMap = apiNameDao.selectApiNames(apiIds);
|
||||||
|
for (ApiStatsUI row : rows) {
|
||||||
|
row.setApiDesc(nameMap.get(row.getApiName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.json", params = "cmd=EXCEL_EXPORT")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.json", params = "cmd=EXCEL_EXPORT")
|
||||||
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
||||||
log.info("Excel export started - search: {}", search);
|
log.info("Excel export started - search: {}", search);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import java.io.IOException;
|
|||||||
import java.time.YearMonth;
|
import java.time.YearMonth;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -18,7 +20,10 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricJpaDAO;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonth;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonth;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonthService;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonthService;
|
||||||
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
||||||
@@ -38,6 +43,7 @@ public class ApiStatsMonthController {
|
|||||||
private final ApiStatsMonthService service;
|
private final ApiStatsMonthService service;
|
||||||
private final ApiStatsUIMapper mapper;
|
private final ApiStatsUIMapper mapper;
|
||||||
private final ApiStatsExcelExportService excelExportService;
|
private final ApiStatsExcelExportService excelExportService;
|
||||||
|
private final ObpGwMetricJpaDAO apiNameDao;
|
||||||
|
|
||||||
@GetMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.view")
|
@GetMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.view")
|
||||||
public String view() {
|
public String view() {
|
||||||
@@ -66,9 +72,38 @@ public class ApiStatsMonthController {
|
|||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.json", params = "cmd=LIST_SUMMARY")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.json", params = "cmd=LIST_SUMMARY")
|
||||||
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
||||||
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
||||||
|
fillApiDesc(page.getContent());
|
||||||
return ResponseEntity.ok(new GridResponse<>(page));
|
return ResponseEntity.ok(new GridResponse<>(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요약 통계 행의 apiName(=API ID)으로 TSEAIHE01 을 조회하여 API 명(EAISVCDESC)을 채운다.
|
||||||
|
* 매칭되는 항목이 없으면 공란으로 둔다.
|
||||||
|
*/
|
||||||
|
private void fillApiDesc(List<ApiStatsUI> rows) {
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> apiIds = rows.stream()
|
||||||
|
.map(ApiStatsUI::getApiName)
|
||||||
|
.filter(id -> id != null && !id.isEmpty())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (apiIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API_STATS_MONTH 와 동일한 APIGW 스키마에서 조회
|
||||||
|
DataSourceContextHolder.setDataSourceType(
|
||||||
|
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
|
||||||
|
|
||||||
|
Map<String, String> nameMap = apiNameDao.selectApiNames(apiIds);
|
||||||
|
for (ApiStatsUI row : rows) {
|
||||||
|
row.setApiDesc(nameMap.get(row.getApiName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.json", params = "cmd=EXCEL_EXPORT")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.json", params = "cmd=EXCEL_EXPORT")
|
||||||
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
||||||
log.info("Excel export started - search: {}", search);
|
log.info("Excel export started - search: {}", search);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.eactive.ext.kjb.statistics;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Year;
|
import java.time.Year;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -17,7 +19,10 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricJpaDAO;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYear;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYear;
|
||||||
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYearService;
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYearService;
|
||||||
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
|
||||||
@@ -37,6 +42,7 @@ public class ApiStatsYearController {
|
|||||||
private final ApiStatsYearService service;
|
private final ApiStatsYearService service;
|
||||||
private final ApiStatsUIMapper mapper;
|
private final ApiStatsUIMapper mapper;
|
||||||
private final ApiStatsExcelExportService excelExportService;
|
private final ApiStatsExcelExportService excelExportService;
|
||||||
|
private final ObpGwMetricJpaDAO apiNameDao;
|
||||||
|
|
||||||
@GetMapping(value = "/onl/kjb/statistics/apiStatsYearMan.view")
|
@GetMapping(value = "/onl/kjb/statistics/apiStatsYearMan.view")
|
||||||
public String view() {
|
public String view() {
|
||||||
@@ -65,9 +71,38 @@ public class ApiStatsYearController {
|
|||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.json", params = "cmd=LIST_SUMMARY")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.json", params = "cmd=LIST_SUMMARY")
|
||||||
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
public ResponseEntity<GridResponse<ApiStatsUI>> selectSummaryList(ApiStatsSearch search, Pageable pageable) {
|
||||||
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
Page<ApiStatsUI> page = service.selectSummaryList(search, pageable);
|
||||||
|
fillApiDesc(page.getContent());
|
||||||
return ResponseEntity.ok(new GridResponse<>(page));
|
return ResponseEntity.ok(new GridResponse<>(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요약 통계 행의 apiName(=API ID)으로 TSEAIHE01 을 조회하여 API 명(EAISVCDESC)을 채운다.
|
||||||
|
* 매칭되는 항목이 없으면 공란으로 둔다.
|
||||||
|
*/
|
||||||
|
private void fillApiDesc(List<ApiStatsUI> rows) {
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> apiIds = rows.stream()
|
||||||
|
.map(ApiStatsUI::getApiName)
|
||||||
|
.filter(id -> id != null && !id.isEmpty())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
if (apiIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API_STATS_YEAR 와 동일한 APIGW 스키마에서 조회
|
||||||
|
DataSourceContextHolder.setDataSourceType(
|
||||||
|
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
|
||||||
|
|
||||||
|
Map<String, String> nameMap = apiNameDao.selectApiNames(apiIds);
|
||||||
|
for (ApiStatsUI row : rows) {
|
||||||
|
row.setApiDesc(nameMap.get(row.getApiName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.json", params = "cmd=EXCEL_EXPORT")
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.json", params = "cmd=EXCEL_EXPORT")
|
||||||
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
||||||
log.info("Excel export started - search: {}", search);
|
log.info("Excel export started - search: {}", search);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import lombok.Data;
|
|||||||
public class ApiStatsUI {
|
public class ApiStatsUI {
|
||||||
private String statTime;
|
private String statTime;
|
||||||
private String apiName;
|
private String apiName;
|
||||||
|
private String apiDesc;
|
||||||
private String gwInstanceId;
|
private String gwInstanceId;
|
||||||
private String bizDivCode;
|
private String bizDivCode;
|
||||||
private String clientId;
|
private String clientId;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
# API Status 자동 탐지 초안 양식
|
# API Status 자동 탐지 초안 양식
|
||||||
#
|
#
|
||||||
# ApiStatusDetectionService 가 자동 생성하는 공지 제목/본문/타임라인 문구를 여기서 관리한다.
|
# ApiStatusDetectionService 가 자동 생성하는 공지와 관리자 수동 등록 공지가 함께 쓰는 문구다.
|
||||||
# 환경별로 갈리는 값이 아니므로 (/WEB-INF/properties 와 달리) 단일 파일이다.
|
# 환경별로 갈리는 값이 아니므로 (/WEB-INF/properties 와 달리) 단일 파일이다.
|
||||||
#
|
#
|
||||||
|
# - 운영 중에는 PTL_PROPERTY (그룹 'Portal', 키 'djb.apistatus.draft.' + 아래 키) 값이 우선한다.
|
||||||
|
# 이 파일은 그 프로퍼티가 없을 때 쓰는 바탕값이자 문구 원본이다.
|
||||||
|
#
|
||||||
# - 치환자는 {name} 형식. 정의되지 않은 키는 치환되지 않고 그대로 남는다.
|
# - 치환자는 {name} 형식. 정의되지 않은 키는 치환되지 않고 그대로 남는다.
|
||||||
# - 이 파일이 없거나 파싱에 실패하면 코드에 박힌 기본 문구로 동작한다 (초안 생성은 멈추지 않는다).
|
# - 이 파일이 없거나 파싱에 실패하면 코드에 박힌 기본 문구로 동작한다 (초안 생성은 멈추지 않는다).
|
||||||
# - 게시되지 않은 GW 인터페이스는 이름·ID 를 노출하지 않고 gw-label 로 묶어 표기한다.
|
# - 게시되지 않은 GW 인터페이스는 이름·ID 를 노출하지 않고 gw-label 로 묶어 표기한다.
|
||||||
@@ -20,12 +23,19 @@ draft:
|
|||||||
multi: "[자동감지] {first} 외 {rest}종 API {keyword}"
|
multi: "[자동감지] {first} 외 {rest}종 API {keyword}"
|
||||||
# 게시된 API 가 하나도 없을 때 (인터페이스 명을 쓰지 않는다)
|
# 게시된 API 가 하나도 없을 때 (인터페이스 명을 쓰지 않는다)
|
||||||
hidden-only: "[자동감지] {gw} {keyword}"
|
hidden-only: "[자동감지] {gw} {keyword}"
|
||||||
|
# 관리자 수동 등록용 - 사람이 직접 쓰는 공지라 "[자동감지]"·GW 묶음 표기를 쓰지 않는다
|
||||||
|
manual-single: "{first} API {keyword}"
|
||||||
|
manual-multi: "{first} 외 {rest}종 API {keyword}"
|
||||||
|
# 영향 API 를 아직 고르지 않은 상태
|
||||||
|
manual-empty: "API {keyword}"
|
||||||
|
|
||||||
# ---- 공지 본문 (HTML) ----
|
# ---- 공지 본문 (HTML) ----
|
||||||
body:
|
body:
|
||||||
# {summary}=탐지 사유(에러율 임계 초과 등)
|
# {summary}=탐지 사유(에러율 임계 초과 등)
|
||||||
lead-publish: "<p>{summary} 로 자동 감지된 장애입니다. 상세 내용은 확인 후 갱신될 수 있습니다.</p>"
|
lead-publish: "<p>{summary} 로 자동 감지된 장애입니다. 상세 내용은 확인 후 갱신될 수 있습니다.</p>"
|
||||||
lead-draft: "<p>{summary} 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.</p>"
|
lead-draft: "<p>{summary} 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.</p>"
|
||||||
|
# 관리자가 공지사항 관리에서 직접 등록할 때 (탐지 사유가 없으므로 치환자를 쓰지 않는다)
|
||||||
|
lead-manual: "<p>[작성 필요] 장애 개요를 입력해 주십시오.</p>"
|
||||||
affected-heading: "영향 API"
|
affected-heading: "영향 API"
|
||||||
# 게시 상태로 나가는 본문에 "작성 필요" 가 그대로 보이면 안 되므로 문구를 나눈다
|
# 게시 상태로 나가는 본문에 "작성 필요" 가 그대로 보이면 안 되므로 문구를 나눈다
|
||||||
filled-text: "확인 중입니다."
|
filled-text: "확인 중입니다."
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<configuration scan="true" scanPeriod="10 seconds">
|
||||||
|
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
|
||||||
|
|
||||||
|
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
|
||||||
|
<property name="PREFIX" value="${inst.Type:-ems}" />
|
||||||
|
<property name="LOG_HOME" value="/work/eactive/djb-eapim/workspace/eapim-admin/logs" />
|
||||||
|
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
|
||||||
|
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
|
||||||
|
|
||||||
|
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="STDOUT"
|
||||||
|
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_HOME}/${PREFIX}_stdout.log</file>
|
||||||
|
<rollingPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>
|
||||||
|
${LOG_HOME}/${PREFIX}_stdout.log-%d{yyyy-MM-dd}.%i
|
||||||
|
</fileNamePattern>
|
||||||
|
<maxFileSize>500MB</maxFileSize>
|
||||||
|
<maxHistory>10</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="ACCESS_APPENDER"
|
||||||
|
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_HOME}/${PREFIX}_access.log</file>
|
||||||
|
<rollingPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>
|
||||||
|
${LOG_HOME}/${PREFIX}_access.log-%d{yyyy-MM-dd}.%i
|
||||||
|
</fileNamePattern>
|
||||||
|
<maxFileSize>500MB</maxFileSize>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<appender name="SMS_APPENDER"
|
||||||
|
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_HOME}/sms.log</file>
|
||||||
|
<rollingPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>
|
||||||
|
${LOG_HOME}/sms.log-%d{yyyy-MM-dd}.%i
|
||||||
|
</fileNamePattern>
|
||||||
|
<maxFileSize>500MB</maxFileSize>
|
||||||
|
<maxHistory>10</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<appender name="QUERY_APPENDER"
|
||||||
|
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_HOME}/${PREFIX}_query.log</file>
|
||||||
|
<rollingPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>
|
||||||
|
${LOG_HOME}/${PREFIX}_query.log-%d{yyyy-MM-dd}.%i
|
||||||
|
</fileNamePattern>
|
||||||
|
<maxFileSize>500MB</maxFileSize>
|
||||||
|
<maxHistory>10</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<appender name="DYNAMIC_APPENDER"
|
||||||
|
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_HOME}/${PREFIX}_dynamic.log</file>
|
||||||
|
<rollingPolicy
|
||||||
|
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>
|
||||||
|
${LOG_HOME}/${PREFIX}_dynamic.log-%d{yyyy-MM-dd}.%i
|
||||||
|
</fileNamePattern>
|
||||||
|
<maxFileSize>500MB</maxFileSize>
|
||||||
|
<maxHistory>10</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<appender name="HIBERNATE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_HOME}/${PREFIX}_hibernate.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_HOME}/${PREFIX}_hibernate.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
|
||||||
|
<maxFileSize>500MB</maxFileSize>
|
||||||
|
<maxHistory>10</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<appender name="HOTSWAP_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_HOME}/${PREFIX}_hotswap.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_HOME}/${PREFIX}_hotswap.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
|
||||||
|
<maxFileSize>500MB</maxFileSize>
|
||||||
|
<maxHistory>10</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<charset>utf-8</charset>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<logger name="com.eactive.eai.rms.onl.server" level="ERROR" />
|
||||||
|
<logger name="com.eactive.eai.rms.onl.dashboard" level="ERROR" />
|
||||||
|
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||||
|
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||||
|
<logger name="com.eactive.eai.rms.ext.djb.job" level="DEBUG" />
|
||||||
|
<logger name="com.eactive.eai.rms.ext.djb.apistatus" level="DEBUG" />
|
||||||
|
|
||||||
|
<logger name="org.springframework.orm.jpa.JpaTransactionManager" level="ERROR" additivity="false" />
|
||||||
|
|
||||||
|
<logger name="org.springframework" level="INFO" />
|
||||||
|
<logger name="net.sf.ehcache" level="INFO" />
|
||||||
|
|
||||||
|
|
||||||
|
<logger name="ACCESS_LOGGER" level="DEBUG" >
|
||||||
|
<appender-ref ref="ACCESS_APPENDER" />
|
||||||
|
</logger>
|
||||||
|
<logger name="SMS_LOGGER" level="DEBUG" >
|
||||||
|
<appender-ref ref="SMS_APPENDER" />
|
||||||
|
</logger>
|
||||||
|
<logger name="DYNAMIC_LOGGER" level="DEBUG" >
|
||||||
|
<appender-ref ref="DYNAMIC_APPENDER" />
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- QUERY_APPENDER start -->
|
||||||
|
<!-- Hibernate SQL & 파라미터 바인딩 -->
|
||||||
|
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
|
||||||
|
<appender-ref ref="QUERY_APPENDER" />
|
||||||
|
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||||
|
</logger>
|
||||||
|
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
|
||||||
|
<appender-ref ref="QUERY_APPENDER" />
|
||||||
|
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||||
|
</logger>
|
||||||
|
<logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="TRACE" additivity="false">
|
||||||
|
<appender-ref ref="QUERY_APPENDER" />
|
||||||
|
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||||
|
</logger>
|
||||||
|
<!-- Hibernate 일반 로그 (DDL/init/connection 등) -->
|
||||||
|
<logger name="org.hibernate" level="INFO" additivity="false">
|
||||||
|
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</logger>
|
||||||
|
|
||||||
|
<!-- iBATIS 2.3.4 (commons-logging 또는 log4j-over-slf4j 통해 SLF4J 도달) -->
|
||||||
|
<logger name="com.ibatis" level="DEBUG" additivity="false">
|
||||||
|
<appender-ref ref="QUERY_APPENDER" />
|
||||||
|
</logger>
|
||||||
|
<logger name="com.eactive.eai.rms.common.base.SchemaIdConfiguredSqlMapTemplate" level="DEBUG" additivity="false">
|
||||||
|
<appender-ref ref="QUERY_APPENDER" />
|
||||||
|
</logger>
|
||||||
|
<!-- QUERY_APPENDER end -->
|
||||||
|
|
||||||
|
<!-- HotswapAgent는 자체 logger를 사용하므로 logback에 안 잡힘. hotswap-agent.properties의 LOGFILE로 직접 파일 출력 -->
|
||||||
|
|
||||||
|
<root>
|
||||||
|
<level value="INFO" />
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
<appender-ref ref="CONSOLE" />
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package com.eactive.eai.rms.data.entity.onl.apim.apispec;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
|
||||||
|
import org.hibernate.SessionFactory;
|
||||||
|
import org.hibernate.cfg.Configuration;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
|
||||||
|
|
||||||
|
class ApiSpecStatusRepositoryTest {
|
||||||
|
private static SessionFactory sessionFactory;
|
||||||
|
private static EntityManager entityManager;
|
||||||
|
private static ApiSpecStatusRepository repository;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUp() {
|
||||||
|
sessionFactory = new Configuration()
|
||||||
|
.addAnnotatedClass(ApiSpecInfo.class)
|
||||||
|
.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect")
|
||||||
|
.setProperty("hibernate.connection.driver_class", "org.h2.Driver")
|
||||||
|
.setProperty("hibernate.connection.url", "jdbc:h2:mem:apiSpecStatus;DB_CLOSE_DELAY=-1")
|
||||||
|
.setProperty("hibernate.hbm2ddl.auto", "create-drop")
|
||||||
|
.buildSessionFactory();
|
||||||
|
entityManager = sessionFactory.createEntityManager();
|
||||||
|
repository = new JpaRepositoryFactory(entityManager).getRepository(ApiSpecStatusRepository.class);
|
||||||
|
entityManager.getTransaction().begin();
|
||||||
|
save("PUBLISHED1", "Y");
|
||||||
|
save("REGISTERED1", "N");
|
||||||
|
save("LEGACY2", null);
|
||||||
|
entityManager.getTransaction().commit();
|
||||||
|
entityManager.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void save(String apiId, String displayYn) {
|
||||||
|
ApiSpecInfo spec = new ApiSpecInfo();
|
||||||
|
spec.setApiId(apiId);
|
||||||
|
spec.setDisplayYn(displayYn);
|
||||||
|
entityManager.persist(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void tearDown() {
|
||||||
|
if (entityManager != null) entityManager.close();
|
||||||
|
if (sessionFactory != null) sessionFactory.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void publishedIncludesOnlyDisplayedSpecs() {
|
||||||
|
assertEquals(Arrays.asList("PUBLISHED1"), repository.findApiIdsByStatus("PUBLISHED"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registeredIncludesNonDisplayedAndLegacyNullSpecs() {
|
||||||
|
assertEquals(new HashSet<>(Arrays.asList("REGISTERED1", "LEGACY2")),
|
||||||
|
new HashSet<>(repository.findApiIdsByStatus("REGISTERED")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void statusProjectionIncludesOnlyRequestedExistingSpecs() {
|
||||||
|
Map<String, String> statuses = repository.findStatusesByApiIds(
|
||||||
|
Arrays.asList("PUBLISHED1", "LEGACY2", "MISSING2")).stream()
|
||||||
|
.collect(Collectors.toMap(ApiSpecStatusRepository.SpecStatus::getApiId,
|
||||||
|
spec -> "Y".equals(spec.getDisplayYn()) ? "PUBLISHED" : "REGISTERED"));
|
||||||
|
assertEquals(2, statuses.size());
|
||||||
|
assertEquals("PUBLISHED", statuses.get("PUBLISHED1"));
|
||||||
|
assertEquals("REGISTERED", statuses.get("LEGACY2"));
|
||||||
|
assertEquals("UNREGISTERED", statuses.getOrDefault("MISSING2", "UNREGISTERED"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingApiHasNoSpecStatus() {
|
||||||
|
assertTrue(repository.findStatusesByApiIds(Arrays.asList("MISSING2")).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user