Compare commits
126 Commits
| 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 | |||
| a2cda26909 | |||
| 598dc5b1ee | |||
| 28ab6863da | |||
| 6e4abf9f27 | |||
| 95ce7f9150 | |||
| 8a7ceacb32 | |||
| 8b22f8e1f8 | |||
| 497b72c52a | |||
| e69e1a1031 | |||
| 9df39e74bd | |||
| 37817b15a9 | |||
| b3f797b726 | |||
| 929ec1f62d | |||
| 720c49295c | |||
| b3b315cd6a | |||
| e0b7ab1129 | |||
| ac90d171d3 | |||
| 0f36718534 | |||
| 314eb87ea4 | |||
| a6e88f2281 | |||
| 38f89fcd7a | |||
| 48073d5d91 | |||
| 79653a2887 | |||
| 97d6489bf7 | |||
| 1bf3a950e3 | |||
| 12e4213fa1 | |||
| e60d5443a3 | |||
| a15fa5ad66 | |||
| 7b8646ab94 | |||
| 26f85b6f11 | |||
| de6f7e5ff2 | |||
| e2206aedc4 | |||
| 2e30239a90 | |||
| e28d6e54c5 | |||
| eb5e289a4e | |||
| ca9c3240bf | |||
| 289f8e2f2f | |||
| a0a1954a58 | |||
| 0149da372e | |||
| 3bb0325bf7 | |||
| 579cb61a75 | |||
| 6c67e3f1d8 | |||
| a19f65a660 | |||
| 0e616bc995 | |||
| 7ac9c05f62 | |||
| 7c9f87db73 | |||
| 8448d99fe8 | |||
| ec9cc19821 | |||
| c2d6eb4c34 | |||
| e364a35b66 | |||
| 760000070d | |||
| 71e433a2a5 | |||
| 5830031f32 | |||
| c7be9760a0 | |||
| ee0b882608 | |||
| 2cdd321c39 | |||
| d1eecb4ee7 | |||
| 91a156c6b0 | |||
| c19c12df70 | |||
| 347cca2d7c | |||
| d8a1ca84e2 | |||
| bf4f953d56 | |||
| 814571eeba | |||
| 13850ba329 | |||
| 04b4d415a9 | |||
| 38902cf6b3 | |||
| 7ebbd7d9e4 | |||
| f59de66d2c | |||
| c179e50047 | |||
| 39f0baf5ef | |||
| 08354837b1 | |||
| a31b991e59 | |||
| 615e56687a | |||
| ba1161be7b | |||
| 7430b03323 | |||
| 5b05489924 | |||
| 3c386b780b | |||
| f928a0b1e4 | |||
| dc9e737aa9 | |||
| db57e95233 | |||
| 643e7fcdf2 | |||
| 36994a57b6 | |||
| 4a087e576b | |||
| 9f22d4fa4d | |||
| 66c01029cd |
@@ -1,7 +1,7 @@
|
||||
UserManController_사용자관리_APIGW_INSERT,UPDATE,DELETE
|
||||
AdapterController_어댑터관리_APIGW_INSERT,UPDATE,DELETE
|
||||
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
|
||||
MenuController_Menu관리_APIGW_INSERT,UPDATE,DELETE
|
||||
PortalApprovalLineManController_승인라인관리_APIGW_INSERT,UPDATE,DELETE
|
||||
|
||||
@@ -15,4 +15,4 @@ eai.tableowner=EMSAPP
|
||||
eai.systemmode=L
|
||||
eai.drmode=N
|
||||
# black / blue / green / orange / yellow
|
||||
theme.color=yellow
|
||||
theme.color=blue
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
</mvc:interceptor>
|
||||
<mvc:interceptor>
|
||||
<mvc:mapping path="/_onl/**"/>
|
||||
<bean class="com.eactive.eai.rms.common.interceptor.ApiKeyInterceptor" />
|
||||
<bean class="com.eactive.eai.rms.common.interceptor.InternalApiTokenInterceptor" />
|
||||
</mvc:interceptor>
|
||||
<!-- BaseRestController 엔드포인트 IP 화이트리스트 인증 -->
|
||||
<mvc:interceptor>
|
||||
|
||||
@@ -175,6 +175,6 @@
|
||||
</jsp-config>
|
||||
|
||||
<session-config>
|
||||
<session-timeout>60</session-timeout>
|
||||
<session-timeout>60</session-timeout>
|
||||
</session-config>
|
||||
</web-app>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<context-root>monitoring</context-root>
|
||||
<session-descriptor>
|
||||
<timeout-secs>1800</timeout-secs>
|
||||
<timeout-secs>3600</timeout-secs>
|
||||
<cookie-name>JSESSIONID_EMS</cookie-name>
|
||||
<persistent-store-type>replicated_if_clustered</persistent-store-type>
|
||||
</session-descriptor>
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
// (enricher 는 oauth·api_key 를 모두 apiKey-in-header 로 모델링하므로 scheme.type 만으로 구분 불가)
|
||||
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. 유틸
|
||||
// ============================================================
|
||||
@@ -191,25 +199,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 경로(op.path.value)에는 어댑터경로가 이미 baking 됨(백엔드 applyAutoRules). 서버에는 응답유형별 호스트만 붙인다.
|
||||
// - gw : djb.gateway.base-url (PortalProperty) → [gw호스트 + 어댑터경로 + 경로]
|
||||
// - mock : Mock Server URL(전체 URL) → [mock URL 그대로] (어댑터/오퍼레이션 경로 미부착)
|
||||
// - 그 외 : 호스트 없음 → [어댑터경로 + 경로]
|
||||
var _rt = (d.portal && d.portal.responseType) || 'sample';
|
||||
var _host;
|
||||
var _pathKey = op.path.value;
|
||||
if (_rt === 'gw') {
|
||||
_host = (window.DJB_CTX && window.DJB_CTX.gwAddress) || 'http://127.0.0.1:39310';
|
||||
} else if (_rt === 'mock') {
|
||||
// Mock 은 전체 호출 URL. origin=서버, pathname(+query)=경로로 분해해 경로가 뒤에 덧붙지 않도록 한다.
|
||||
var _mk = splitFullUrl((d.portal && d.portal.mockUrl) || '');
|
||||
_host = _mk.origin;
|
||||
_pathKey = _mk.path;
|
||||
} else {
|
||||
_host = '';
|
||||
}
|
||||
if (_host) spec.servers = [{ url: _host }];
|
||||
spec.paths[_pathKey] = { [op.method.value.toLowerCase()]: operation };
|
||||
// spec 의 서버는 응답유형과 무관하게 항상 GW 주소로 고정한다 — spec 은 "이 API 를 실제로 어디로
|
||||
// 호출하는가"의 공개 계약이고, 그 기준은 게이트웨이다. sample/mock 은 테스트베드 동작(포탈
|
||||
// ApiTesterFilter 가 응답유형에 따라 sampleResponse 반환 / mockUrl forward)에서만 갈린다.
|
||||
// 경로(op.path.value)에는 어댑터경로가 이미 baking 됨(백엔드 applyAutoRules).
|
||||
spec.servers = [{ url: gatewayAddress() }];
|
||||
spec.paths[op.path.value] = { [op.method.value.toLowerCase()]: operation };
|
||||
|
||||
// 보안 스킴
|
||||
d.securitySchemes.forEach(s => {
|
||||
@@ -237,6 +232,8 @@
|
||||
const s = { type: row.type.value };
|
||||
if (row.format && row.format.value) s.format = row.format.value;
|
||||
if (row.maxLength && row.maxLength.value) s.maxLength = Number(row.maxLength.value);
|
||||
// 소수 자릿수(GW 레이아웃 유래) 보존 — 예제값 생성 시 '.' 포함 길이 계산에 사용
|
||||
if (row.decimals && row.decimals.value !== '' && row.decimals.value != null) s['x-djb-decimal'] = Number(row.decimals.value);
|
||||
if (row.pattern && row.pattern.value) s.pattern = row.pattern.value;
|
||||
if (row.description && row.description.value) s.description = row.description.value;
|
||||
if (row.example && row.example.value !== '') s.example = coerceExample(row.type.value, row.example.value);
|
||||
@@ -981,6 +978,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// spec servers 에 넣을 게이트웨이 주소(단일 기준). 응답유형과 무관하게 항상 이 값을 쓴다.
|
||||
function gatewayAddress() {
|
||||
return (window.DJB_CTX && window.DJB_CTX.gwAddress) || 'http://127.0.0.1:39310';
|
||||
}
|
||||
|
||||
// 전체 URL → { origin, path }. Mock Server URL 을 서버+경로로 분해(경로를 별도로 덧붙이지 않기 위함).
|
||||
function splitFullUrl(u) {
|
||||
u = (u || '').trim();
|
||||
@@ -1474,6 +1476,7 @@
|
||||
required: { value: false, locked: false },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
decimals: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
@@ -1488,6 +1491,7 @@
|
||||
required: { value: false, locked: false },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
decimals: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '', locked: false }
|
||||
@@ -1707,7 +1711,7 @@
|
||||
|
||||
var bSave = $('#btn-save'); if (bSave) bSave.addEventListener('click', () => saveSpec(false));
|
||||
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 → 저장 (브라우저 기본 저장 대화상자 차단)
|
||||
document.addEventListener('keydown', function (e) {
|
||||
@@ -1741,6 +1745,7 @@
|
||||
var row = {
|
||||
name: wrap(name, gw), type: wrap(t, gw), required: wrap(!!required, false),
|
||||
format: wrap(p.format || ''), maxLength: wrap(p.maxLength == null ? '' : p.maxLength),
|
||||
decimals: wrap(p['x-djb-decimal'] == null ? '' : p['x-djb-decimal']),
|
||||
pattern: wrap(p.pattern || ''), description: wrap(p.description || ''),
|
||||
example: wrap(p.example == null ? '' : p.example), children: null
|
||||
};
|
||||
@@ -1790,7 +1795,7 @@
|
||||
};
|
||||
d.parameters = (op.parameters || []).map(function (p) {
|
||||
var s = p.schema || {};
|
||||
return { in: p.in || 'query', name: wrap(p.name), type: wrap(s.type || 'string'), required: wrap(!!p.required), format: wrap(s.format || ''), maxLength: wrap(s.maxLength == null ? '' : s.maxLength), pattern: wrap(s.pattern || ''), description: wrap(p.description || ''), example: wrap(p.example == null ? '' : p.example) };
|
||||
return { in: p.in || 'query', name: wrap(p.name), type: wrap(s.type || 'string'), required: wrap(!!p.required), format: wrap(s.format || ''), maxLength: wrap(s.maxLength == null ? '' : s.maxLength), decimals: wrap(s['x-djb-decimal'] == null ? '' : s['x-djb-decimal']), pattern: wrap(s.pattern || ''), description: wrap(p.description || ''), example: wrap(p.example == null ? '' : p.example) };
|
||||
});
|
||||
var rb = op.requestBody || {}; var rbc = rb.content || {}; var mt = Object.keys(rbc)[0] || 'application/json';
|
||||
d.requestBody = { mediaType: mt, required: wrap(!!rb.required), schema: schemaToRows((rbc[mt] || {}).schema) };
|
||||
@@ -1835,19 +1840,34 @@
|
||||
|
||||
// 스키마 기반 샘플 JSON 생성 (자동생성 예제)
|
||||
function _coerce(t, v) { if (t === 'integer') return parseInt(v, 10) || 0; if (t === 'number') return parseFloat(v) || 0; if (t === 'boolean') return v === true || v === 'true'; return v; }
|
||||
// 기본 예제값: string→sample_명(길이 지정 시 맞춰 자름), integer→123456, number→123456.123, boolean→false
|
||||
// - 숫자는 길이 지정 시 정수부를 그 길이에 맞춰 자름(실수는 소수 .123 유지)
|
||||
function _defScalar(t, name, maxLen) {
|
||||
var ml = parseInt(maxLen, 10);
|
||||
if (t === 'integer') {
|
||||
var iv = '123456';
|
||||
if (ml && ml > 0 && iv.length > ml) iv = iv.substring(0, ml);
|
||||
return parseInt(iv, 10);
|
||||
// 자릿수 문자열: start 위치부터 1~9 를 순환('123456789123...')
|
||||
function _digits(start, count) { var s = ''; for (var i = 0; i < count; i++) s += String(((start + i) % 9) + 1); return s; }
|
||||
// 숫자 예제값. 길이는 소수점(.)까지 포함한 문자열 전체 길이로 계산한다.
|
||||
// 예) 길이 9 / 소수 5 → 123.45678 (정수부 3 + '.' 1 + 소수부 5 = 9자)
|
||||
// - 길이가 소수부+2 보다 짧으면 정수부 1자리를 확보하고 소수부를 줄인다.
|
||||
// - 길이 미지정이면 정수부 6자리(기존 기본값)를 사용한다.
|
||||
function _numSample(maxLen, dec) {
|
||||
var ml = parseInt(maxLen, 10) || 0, d = parseInt(dec, 10) || 0;
|
||||
if (d <= 0) return _digits(0, ml > 0 ? Math.min(ml, 6) : 6);
|
||||
var il;
|
||||
if (ml > 0) {
|
||||
il = ml - d - 1;
|
||||
if (il < 1) { il = 1; d = Math.max(1, ml - 2); }
|
||||
} else {
|
||||
il = 6;
|
||||
}
|
||||
return _digits(0, il) + '.' + _digits(il, d);
|
||||
}
|
||||
// 기본 예제값: string→sample_명(길이 지정 시 맞춰 자름), integer→123456, number→123.45678, boolean→false
|
||||
// - 숫자는 지정 길이를 넘지 않도록 자릿수를 맞춘다(소수는 '.' 포함 길이 기준).
|
||||
// - number 인데 소수 자릿수 정보가 없으면 3자리로 가정(소수 0자리는 백엔드가 integer 로 매핑).
|
||||
function _defScalar(t, name, maxLen, decimals) {
|
||||
var ml = parseInt(maxLen, 10);
|
||||
if (t === 'integer') return parseInt(_numSample(ml, 0), 10);
|
||||
if (t === 'number') {
|
||||
var ip = '123456';
|
||||
if (ml && ml > 0 && ip.length > ml) ip = ip.substring(0, ml);
|
||||
return parseFloat(ip + '.123');
|
||||
var d = parseInt(decimals, 10);
|
||||
if (!(d > 0)) d = 3;
|
||||
return parseFloat(_numSample(ml, d));
|
||||
}
|
||||
if (t === 'boolean') return false;
|
||||
var v = 'sample_' + (name || '');
|
||||
@@ -1859,7 +1879,7 @@
|
||||
if (t === 'object') return _sampleObj(row.children || []);
|
||||
if (t === 'array') { var it = (row.itemsType && row.itemsType.value) || 'string'; return [it === 'object' ? _sampleObj(row.children || []) : _defScalar(it, (row.name && row.name.value) || 'item', '')]; }
|
||||
if (ex !== undefined && ex !== '') return _coerce(t, ex);
|
||||
return _defScalar(t, (row.name && row.name.value) || '', row.maxLength && row.maxLength.value);
|
||||
return _defScalar(t, (row.name && row.name.value) || '', row.maxLength && row.maxLength.value, row.decimals && row.decimals.value);
|
||||
}
|
||||
function _sampleObj(rows) { var o = {}; (rows || []).forEach(function (r) { if (r && r.name && r.name.value) o[r.name.value] = _sampleVal(r); }); return o; }
|
||||
function ensureExamplesFromSchema(force) {
|
||||
@@ -1905,7 +1925,7 @@
|
||||
if (r.children && r.children.length) fillSchemaExamples(r.children);
|
||||
} else {
|
||||
r.example = r.example || { value: '', locked: false };
|
||||
r.example.value = String(_defScalar(t, r.name.value || '', r.maxLength && r.maxLength.value));
|
||||
r.example.value = String(_defScalar(t, r.name.value || '', r.maxLength && r.maxLength.value, r.decimals && r.decimals.value));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2054,6 +2074,7 @@
|
||||
.then(function (res) {
|
||||
if (res && res.status === 'success') {
|
||||
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'); }
|
||||
} else toast('저장 실패: ' + (res && res.errorMsg || ''), 'error');
|
||||
})
|
||||
@@ -2090,6 +2111,7 @@
|
||||
// ============================================================
|
||||
function init() {
|
||||
loadInitialData().then(function () {
|
||||
savedSnapshot = JSON.stringify(state.data); // 초기 로드(자동생성 포함) 완료 시점을 저장 기준선으로 설정
|
||||
renderStepper();
|
||||
renderForm();
|
||||
bindGlobalEvents();
|
||||
|
||||
@@ -44,10 +44,14 @@ $(document).ready(function() {
|
||||
$(this).val(getEndTime());
|
||||
}
|
||||
});
|
||||
|
||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
gridPostData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val() + $("input[name=searchStartHHMM]").val();
|
||||
gridPostData['searchEndDateTime'] = $("input[name=searchEndYYYYMMDD]").val() + $("input[name=searchEndHHMM]").val();
|
||||
|
||||
//검색조건 달력
|
||||
$("#startDatepicker").datepicker();
|
||||
$("#endDatepicker").datepicker();
|
||||
|
||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
gridPostData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "") + $("input[name=searchStartHHMM]").val();
|
||||
gridPostData['searchEndDateTime'] = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi, "") + $("input[name=searchEndHHMM]").val();
|
||||
$('#grid').jqGrid({
|
||||
datatype:"json",
|
||||
mtype: 'POST',
|
||||
@@ -106,9 +110,9 @@ $(document).ready(function() {
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
postData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val() + $("input[name=searchStartHHMM]").val();
|
||||
postData['searchEndDateTime'] = $("input[name=searchEndYYYYMMDD]").val() + $("input[name=searchEndHHMM]").val();
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
postData['searchStartDateTime'] = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "") + $("input[name=searchStartHHMM]").val();
|
||||
postData['searchEndDateTime'] = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi, "") + $("input[name=searchEndHHMM]").val();
|
||||
$("#grid").setGridParam({ postData: postData ,page : "1" }).trigger("reloadGrid");
|
||||
|
||||
});
|
||||
@@ -154,11 +158,11 @@ function getEndTime(){
|
||||
<tr>
|
||||
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.logamndHMS") %></th>
|
||||
<td>
|
||||
<input type="text" name="searchStartYYYYMMDD" value="" size="10" style="width:100px; border:1px solid #ebebec;">
|
||||
<input type="text" name="searchStartHHMM" value="" size="8" style="width:100px; border:1px solid #ebebec;">
|
||||
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" value="${param.searchStartYYYYMMDD}" readonly="readonly" size="10" style="width:100px; border:1px solid #ebebec;">
|
||||
<input type="text" name="searchStartHHMM" value="${param.searchStartHHMM}" size="8" style="width:100px; border:1px solid #ebebec;">
|
||||
~
|
||||
<input type="text" name="searchEndYYYYMMDD" value="" size="10" style="width:100px; border:1px solid #ebebec;">
|
||||
<input type="text" name="searchEndHHMM" value="" size="8" style="width:100px; border:1px solid #ebebec;">
|
||||
<input type="text" name="searchEndYYYYMMDD" id="endDatepicker" value="${param.searchEndYYYYMMDD}" readonly="readonly" size="10" style="width:100px; border:1px solid #ebebec;">
|
||||
<input type="text" name="searchEndHHMM" value="${param.searchEndHHMM}" size="8" style="width:100px; border:1px solid #ebebec;">
|
||||
<input type="hidden" name="searchStartDateTime" />
|
||||
<input type="hidden" name="searchEndDateTime" />
|
||||
</td>
|
||||
@@ -168,9 +172,9 @@ function getEndTime(){
|
||||
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.systemCode") %></th>
|
||||
<td><div class = "select-style">
|
||||
<select id="searchSystemCode" name="searchSystemCode">
|
||||
<option value=""><%= localeMessage.getString("combo.all")%></option>
|
||||
<option value="" <c:if test="${empty param.searchSystemCode}">selected</c:if>><%= localeMessage.getString("combo.all")%></option>
|
||||
<c:forEach items="${serviceTypeCombos }" var="serviceTypeCombo">
|
||||
<option value="${serviceTypeCombo.code }">${serviceTypeCombo.codename }</option>
|
||||
<option value="${serviceTypeCombo.code }" <c:if test="${serviceTypeCombo.code == param.searchSystemCode}">selected</c:if>>${serviceTypeCombo.codename }</option>
|
||||
</c:forEach>
|
||||
</select>
|
||||
</div>
|
||||
@@ -180,7 +184,7 @@ function getEndTime(){
|
||||
|
||||
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.logTypeText") %></th>
|
||||
<td>
|
||||
<input type="text" value="" name="searchLogTypeText" checked="checked"/>
|
||||
<input type="text" value="${param.searchLogTypeText}" name="searchLogTypeText" checked="checked"/>
|
||||
</td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.remoteAddress") %></th>
|
||||
<td><input type="text" name="searchRemoteAddress" value="${param.searchRemoteAddress}" style="width:100%"></td>
|
||||
|
||||
@@ -31,8 +31,12 @@
|
||||
<script language="javascript" src="<c:url value="/js/custom.js"/>"></script>
|
||||
<script language="javascript" src="<c:url value="/js/common-${locale}.js"/>"></script>
|
||||
<script type="text/javascript">
|
||||
/* 자동 로그아웃: iframe 내 활동을 sessionStorage에 기록 (main.jsp 타이머와 공유) */
|
||||
(function() { var m = function() { sessionStorage.setItem('lastActivity', String(Date.now())); }; ['mousemove','keydown','click','touchstart'].forEach(function(e){ document.addEventListener(e, m, true); }); })();
|
||||
/* 자동 로그아웃: 서버로 실제 요청이 발생한 경우에만 활동으로 기록 (main.jsp 타이머와 sessionStorage 공유) */
|
||||
(function() {
|
||||
var mark = function() { sessionStorage.setItem('lastActivity', String(Date.now())); };
|
||||
mark(); // 화면(프레임) 로드 자체가 서버 요청이므로 활동으로 기록
|
||||
$(document).ajaxSend(mark); // 이후 이 프레임에서 발생하는 모든 AJAX 요청을 활동으로 기록
|
||||
})();
|
||||
</script>
|
||||
<script language="javascript" src="<c:url value="/js/jquery.inputmask.bundle.min.js"/>"></script>
|
||||
<script language="javascript" src="<c:url value="/js/jquery.searchabledropdown-1.0.8.min.js"/>"></script>
|
||||
|
||||
@@ -28,9 +28,11 @@ function init(){
|
||||
success:function(json){
|
||||
new makeOptions("CODE","NAME").setObj($("select[name=searchJobUseYn]")).setNoValueInclude(true).setNoValue("",
|
||||
"<%= localeMessage.getString("combo.all")%>").setData(json.useYnTypeRows).setFormat(codeName3OptionFormat).rendering();
|
||||
new makeOptions("CODE","NAME").setObj($("select[name=searchJobInst]")).setNoValueInclude(true).setNoValue("",
|
||||
new makeOptions("CODE","NAME").setObj($("select[name=searchJobInst]")).setNoValueInclude(true).setNoValue("",
|
||||
"<%= localeMessage.getString("combo.all")%>").setData(json.instanceList).rendering();
|
||||
|
||||
|
||||
$("select[name=searchJobUseYn]").val("Y");
|
||||
|
||||
putSelectFromParam();
|
||||
|
||||
initGrid();
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
// 비밀번호 규칙 안내문구용 모니터링 프로퍼티 조회
|
||||
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" />
|
||||
|
||||
<%
|
||||
@@ -65,6 +78,7 @@
|
||||
// SMS 인증 관련 변수
|
||||
var smsAuthTimer = null;
|
||||
var resendTimer = null;
|
||||
var smsAuthPurpose = 'LOGIN'; // 'LOGIN' | 'PASSWORD_CHANGE' - SMS 인증 모달을 어느 흐름에서 띄웠는지 구분
|
||||
|
||||
function fncPreMain() {
|
||||
if ($("input[name=userId]").val() ==null || $("input[name=userId]").val().length == 0 ){
|
||||
@@ -91,18 +105,26 @@
|
||||
data: $('#loginForm').serialize(),
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
console.log('res', response)
|
||||
if (response.changePassword) {
|
||||
showChangeInitPassword(response);
|
||||
}
|
||||
else if (response.smsAuthRequired) {
|
||||
// SMS 인증 필요
|
||||
smsAuthPurpose = 'LOGIN';
|
||||
showSmsAuthModal(response);
|
||||
} else if (response.success) {
|
||||
// 직접 로그인 성공
|
||||
window.location.href = response.redirectUrl;
|
||||
} else {
|
||||
// 로그인 실패
|
||||
alert(response.errorMessage || '로그인에 실패했습니다.');
|
||||
if (response.errorMessage == 'NO_PHONE_NUMBER') {
|
||||
alert("휴대폰번호가 등록되어 있지 않아 로그인할 수 없습니다. 관리자에게 문의하세요")
|
||||
} else if (response.errorMessage == 'FAIL_SEND_AUTH') {
|
||||
alert("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.")
|
||||
} else {
|
||||
// 로그인 실패
|
||||
alert('로그인에 실패했습니다');
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
@@ -123,11 +145,21 @@
|
||||
function showSmsAuthModal(data) {
|
||||
$('#smsAuthMaskedPhone').text(data.maskedPhone);
|
||||
$('#smsAuthCode').val('');
|
||||
updateSmsTestHint(data.smsTestHint);
|
||||
startExpiryTimer(data.expiresAt);
|
||||
startResendTimer(data.resendableAt);
|
||||
$('#smsAuthModal').modal('show');
|
||||
}
|
||||
|
||||
// fixed 모드(djb.sms_auth.mode=fixed)일 때만 서버가 내려주는 테스트용 인증번호 힌트 표시
|
||||
function updateSmsTestHint(hint) {
|
||||
if (hint) {
|
||||
$('#smsAuthTestHint').text(hint).show();
|
||||
} else {
|
||||
$('#smsAuthTestHint').text('').hide();
|
||||
}
|
||||
}
|
||||
|
||||
// 유효시간 타이머
|
||||
function startExpiryTimer(expiresAt) {
|
||||
clearInterval(smsAuthTimer);
|
||||
@@ -181,6 +213,7 @@
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
if (data.sent) {
|
||||
updateSmsTestHint(data.smsTestHint);
|
||||
startExpiryTimer(data.expiresAt);
|
||||
startResendTimer(data.resendableAt);
|
||||
alert('인증번호가 재발송되었습니다.');
|
||||
@@ -194,7 +227,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
// SMS 인증번호 확인
|
||||
// SMS 인증번호 확인 (로그인 / 비밀번호 변경 공용 - smsAuthPurpose로 분기)
|
||||
function verifySmsAuthCode() {
|
||||
var code = $('#smsAuthCode').val();
|
||||
if (!code || code.length !== 6) {
|
||||
@@ -203,17 +236,25 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var isPasswordChange = (smsAuthPurpose === 'PASSWORD_CHANGE');
|
||||
var verifyUrl = isPasswordChange
|
||||
? '<c:url value="/sms-auth/verify-password-change.json"/>'
|
||||
: '<c:url value="/sms-auth/verify.json"/>';
|
||||
|
||||
$.ajax({
|
||||
url: '<c:url value="/sms-auth/verify.json"/>',
|
||||
url: verifyUrl,
|
||||
type: 'POST',
|
||||
data: { authCode: code },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
if (data.success) {
|
||||
$('#smsAuthModal').modal('hide');
|
||||
if (isPasswordChange) {
|
||||
alert(data.message);
|
||||
}
|
||||
window.location.href = data.redirectUrl;
|
||||
} else {
|
||||
alert(data.errorMessage || '인증에 실패했습니다.');
|
||||
alert(data.errorMessage || data.message || '인증에 실패했습니다.');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
@@ -262,7 +303,47 @@
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// 비밀번호 변경 요청 (AJAX) - 서버 룰 검증 실패 시 모달을 닫지 않고 비밀번호 입력값만 초기화
|
||||
function submitChangePassword() {
|
||||
if (!checkPwd()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<c:url value="/changePassword.do"/>',
|
||||
type: 'POST',
|
||||
data: $('#modalLoginForm').serialize(),
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.smsAuthRequired) {
|
||||
// 비밀번호는 아직 반영되지 않음 - SMS 인증 완료 시 최종 반영됨
|
||||
smsAuthPurpose = 'PASSWORD_CHANGE';
|
||||
// 두 모달이 겹치면 이전 모달의 enforceFocus/backdrop 이 남아 입력이 막히므로
|
||||
// #pwdChgModal 이 완전히 닫힌 뒤 SMS 인증 모달을 연다
|
||||
$('#pwdChgModal').one('hidden.bs.modal', function() {
|
||||
showSmsAuthModal(response);
|
||||
});
|
||||
$('#pwdChgModal').modal('hide');
|
||||
return;
|
||||
}
|
||||
alert(response.message);
|
||||
if (response.success) {
|
||||
window.location.href = response.redirectUrl || '<c:url value="/emergency.jsp"/>';
|
||||
} else {
|
||||
$("input[name=changePassword]").val('');
|
||||
$("input[name=confirmPassword]").val('');
|
||||
$("input[name=changePassword]").trigger("focus");
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert("<%= localeMessage.getString("login.pwdchangefail") %>");
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$("input[name=userId]").keydown(function(event){
|
||||
if ( event.which == 13 ) {
|
||||
@@ -276,12 +357,14 @@
|
||||
});
|
||||
$("input[name=changePassword]").keydown(function(event){
|
||||
if ( event.which == 13 ) {
|
||||
$('#modalLoginForm').submit();
|
||||
event.preventDefault();
|
||||
submitChangePassword();
|
||||
}
|
||||
});
|
||||
$("input[name=confirmPassword]").keydown(function(event){
|
||||
if ( event.which == 13 ) {
|
||||
$('#modalLoginForm').submit();
|
||||
event.preventDefault();
|
||||
submitChangePassword();
|
||||
}
|
||||
});
|
||||
// Password Change 링크로 직접 열 때만 초기화 (JS에서 modal('show') 호출 시 relatedTarget 없음)
|
||||
@@ -310,6 +393,19 @@
|
||||
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() {
|
||||
@@ -387,7 +483,7 @@
|
||||
<span aria-hidden="true">X</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return checkPwd()" novalidate>
|
||||
<form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return submitChangePassword()" novalidate>
|
||||
<div class="modal-body">
|
||||
<div id="changeInitPassword">
|
||||
<div class="form-group d-flex">
|
||||
@@ -405,7 +501,8 @@
|
||||
<input type="password" name="confirmPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderConfirmationPassword") %>" autocomplete="off" required>
|
||||
</div>
|
||||
<span style="font-size:0.8em; color:red">
|
||||
※ 7글자 이상 & 영문/숫자/특수문자 2종류 이상
|
||||
※ <%= pwdLengthCheck %>글자 이상 & 영문/숫자/특수문자 <%= pwdCombiCheck %>종류 이상<br/>
|
||||
※ 연속된 숫자/문자(예: 123, abc, qwer) 사용 불가<%= pwdRepeatCheck >= 2 ? ", 동일 문자 " + pwdRepeatCheck + "자 이상 반복 불가" : "" %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -427,9 +524,9 @@
|
||||
<p>인증번호가 <strong><span id="smsAuthMaskedPhone"></span></strong>로 발송되었습니다.</p>
|
||||
<div class="form-group">
|
||||
<input type="text" id="smsAuthCode" class="form-control"
|
||||
placeholder="인증번호 6자리" maxlength="6"
|
||||
onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
||||
placeholder="인증번호 6자리" maxlength="6" inputmode="numeric" autocomplete="off">
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
@@ -166,10 +166,8 @@
|
||||
location.href = '<%=request.getContextPath()%>/rms/logout.do';
|
||||
}
|
||||
|
||||
/* 상위 프레임(main.jsp) 활동 감지 */
|
||||
['mousemove', 'keydown', 'click', 'touchstart'].forEach(function (e) {
|
||||
document.addEventListener(e, markActivity, true);
|
||||
});
|
||||
/* 상위 프레임(main.jsp)에서 발생하는 AJAX 요청만 활동으로 기록 (마우스 이동/클릭만으로는 갱신되지 않음) */
|
||||
$(document).ajaxSend(markActivity);
|
||||
|
||||
/* 주기적 유휴 시간 체크 */
|
||||
setInterval(function () {
|
||||
|
||||
@@ -555,7 +555,35 @@
|
||||
<% session.setAttribute("dualLogin", "N"); %>
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// 세션(자동로그아웃) 타임아웃 카운트다운 - main.jsp의 유휴시간 판단과 동일한 sessionStorage 값을 공유한다.
|
||||
(function () {
|
||||
var IDLE_MS = ${autoLogoutTimeout} * 60 * 1000;
|
||||
|
||||
function getLastActivity() {
|
||||
var t = sessionStorage.getItem('lastActivity');
|
||||
return t ? parseInt(t, 10) : Date.now();
|
||||
}
|
||||
|
||||
function formatRemain(ms) {
|
||||
if (ms < 0) ms = 0;
|
||||
var totalSec = Math.floor(ms / 1000);
|
||||
var mm = Math.floor(totalSec / 60);
|
||||
var ss = totalSec % 60;
|
||||
return (mm < 10 ? '0' + mm : mm) + ':' + (ss < 10 ? '0' + ss : ss);
|
||||
}
|
||||
|
||||
function updateSessionTimeoutTimer() {
|
||||
var remain = IDLE_MS - (Date.now() - getLastActivity());
|
||||
$('#sessionTimeoutTimer').text('세션 만료까지 ' + formatRemain(remain));
|
||||
}
|
||||
|
||||
$(function () {
|
||||
updateSessionTimeoutTimer();
|
||||
setInterval(updateSessionTimeoutTimer, 1000);
|
||||
});
|
||||
})();
|
||||
|
||||
</script>
|
||||
</head>
|
||||
@@ -568,11 +596,12 @@
|
||||
<ul>
|
||||
<li style="width:240px;">
|
||||
<a style="width:240px; cursor: default"
|
||||
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>-<%=System.getProperty("inst.Name") %>)</span>
|
||||
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>)</span>
|
||||
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
|
||||
<% if (!"".equals(LastLoginYms.trim())) { %>
|
||||
<span style="display:block;font-size:10px;"><%=localeMessage.getString("screen.lastLogin") %> : <%=LastLoginYms%> [ <%=LastLoginIp%> ]</span>
|
||||
<span style="display:block;font-size:10px;"><%=localeMessage.getString("screen.lastLogin") %> : <%=LastLoginYms%></span>
|
||||
<% } %>
|
||||
<span id="sessionTimeoutTimer" style="display:block;font-size:10px;"></span>
|
||||
</a></li>
|
||||
<li style="width:100px; line-height:80px; text-align:center;">
|
||||
<select id="selectLocale" name="locale" style="width:60px; padding: 2px; text-align:center;">
|
||||
|
||||
@@ -179,8 +179,8 @@
|
||||
autowidth: true,
|
||||
viewrecords : true,
|
||||
gridview : true,
|
||||
ondblClickRow : function(rowId) {
|
||||
console.log("rowId", rowId);
|
||||
ondblClickRow : function(rowId) {
|
||||
/* console.log("rowId", rowId);
|
||||
var rowData = $(this)
|
||||
.getRowData(rowId);
|
||||
var key = rowData['EaiSevrInstncName'];
|
||||
@@ -193,7 +193,7 @@
|
||||
url2 += "&searchAdptrBzwkGroupName="
|
||||
+ key2;
|
||||
url2 += '&menuId='+'${param.menuId}';
|
||||
goNav(url2);
|
||||
goNav(url2); */
|
||||
},
|
||||
loadComplete : function(d) {
|
||||
$("button[name*=img]").unbind("click");
|
||||
|
||||
@@ -190,6 +190,8 @@
|
||||
var key = "${param.clientId}";
|
||||
if (key != "" && key != "null") {
|
||||
isDetail = true;
|
||||
} else {
|
||||
$('input[name="grantTypes_"]').prop('checked', true);
|
||||
}
|
||||
init(key, detail);
|
||||
|
||||
@@ -306,7 +308,8 @@
|
||||
dataType: "json",
|
||||
data: {
|
||||
cmd: 'SYNC_PORTAL',
|
||||
clientId: clientId
|
||||
clientId: clientId,
|
||||
serviceType: 'APIGW'
|
||||
},
|
||||
success: function(response) {
|
||||
showSyncResultModal(response);
|
||||
@@ -451,8 +454,8 @@
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<!-- <button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
||||
</button> -->
|
||||
<button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
||||
</button>
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %>
|
||||
</button>
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i
|
||||
@@ -506,18 +509,17 @@
|
||||
<td><input type="text" name="scope"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("clntManDtl.grntTp") %>
|
||||
(<%= localeMessage.getString("clntManDtl.sprt") %> ,) <small>(*)</small></th>
|
||||
<th><%= localeMessage.getString("clntManDtl.grntTp") %> <small>(*)</small></th>
|
||||
<td>
|
||||
<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="password"> password--%>
|
||||
<input type="checkbox" name="grantTypes_" value="refresh_token"> refresh_token
|
||||
<!-- <input type="checkbox" name="grantTypes_" value="refresh_token"> refresh_token -->
|
||||
</td>
|
||||
<input type="hidden" name="grantTypes"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr style="display: none;">
|
||||
<th>REDIRECT URI</th>
|
||||
<td><input type="text" name="redirectUri"/></td>
|
||||
</tr>
|
||||
@@ -538,7 +540,7 @@
|
||||
</th>
|
||||
<td><input type="text" name="accessTokenValiditySeconds"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr style="display: none;">
|
||||
<th><%= localeMessage.getString("clntManDtl.rfrshTknExprtnDt") %>
|
||||
</th>
|
||||
<td><input type="text" name="refreshTokenValiditySeconds"/></td>
|
||||
@@ -555,7 +557,7 @@
|
||||
</th>
|
||||
<td><input type="text" name="allowedIps"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr style="display: none;">
|
||||
<th>Security Key</th>
|
||||
<td><textarea name="securityKey" rows="3" style="width:100%;"></textarea></td>
|
||||
</tr>
|
||||
@@ -740,7 +742,7 @@ function showSyncResultModal(response) {
|
||||
if (response.success) {
|
||||
statusClass = "status-success";
|
||||
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') {
|
||||
statusMsg = "<strong>반영 완료</strong> - 포탈에 새로운 인증정보가 생성되었습니다." + createdMsg;
|
||||
@@ -770,6 +772,7 @@ function showSyncResultModal(response) {
|
||||
var syncedApis = response.syncedApis || [];
|
||||
var invalidApis = response.invalidApis || [];
|
||||
var createdApiSpecs = response.createdApiSpecs || [];
|
||||
var specGenFailed = response.specGenFailed || [];
|
||||
|
||||
$targetCount.text(targetApis.length);
|
||||
|
||||
@@ -783,8 +786,11 @@ function showSyncResultModal(response) {
|
||||
if (invalidApis.includes(apiId)) {
|
||||
status = "미존재 (APIGW에 없음)";
|
||||
statusClass = "api-invalid";
|
||||
} else if (specGenFailed.includes(apiId)) {
|
||||
status = "반영됨 (Spec 자동생성 실패·빈 항목)";
|
||||
statusClass = "api-created";
|
||||
} else if (createdApiSpecs.includes(apiId)) {
|
||||
status = "반영됨 (Spec 신규생성)";
|
||||
status = "반영됨 (Spec 자동생성·비공개)";
|
||||
statusClass = "api-created";
|
||||
} else if (syncedApis.includes(apiId)) {
|
||||
status = "반영됨";
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/admin/rule/layoutSyncHistoryMan.json" />';
|
||||
var url_view = '<c:url value="/onl/admin/rule/layoutSyncHistoryMan.view" />';
|
||||
|
||||
function formatPrcssRslt(cellvalue, options, rowObject) {
|
||||
if (cellvalue === 'S') return '<span style="color:#1a73e8;font-weight:bold;">성공</span>';
|
||||
if (cellvalue === 'F') return '<span style="color:#d93025;font-weight:bold;">실패</span>';
|
||||
return cellvalue || '';
|
||||
}
|
||||
|
||||
function init() {
|
||||
$("#startDatepicker").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
|
||||
$("#endDatepicker").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
|
||||
|
||||
var today = getToday();
|
||||
|
||||
$("input[name=searchStartYYYYMMDD]").val(today.substring(0, 6) + "01");
|
||||
$("input[name=searchEndYYYYMMDD]").val(today);
|
||||
$("input[name=searchStartDate]").val(today.substring(0, 6) + "01");
|
||||
$("input[name=searchEndDate]").val(today);
|
||||
|
||||
// 뒤로가기 등으로 이전 검색조건 파라미터가 있으면 기본값을 덮어씀
|
||||
putSelectFromParam();
|
||||
|
||||
buildGrid();
|
||||
}
|
||||
|
||||
function buildGrid() {
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['No', '서비스명', 'Command', '연동구분명', '연동시간', '결과', '결과메세지'],
|
||||
colModel: [
|
||||
{name: 'seqno', align: 'center', width: 50, sortable: false},
|
||||
{name: 'serviceName', align: 'center', width: 100, sortable: false},
|
||||
{name: 'command', align: 'center', width: 80, sortable: false},
|
||||
{name: 'loutName', align: 'left', width: 180, sortable: false},
|
||||
{name: 'recvAmndHms', align: 'center', width: 140, sortable: false},
|
||||
{name: 'prcssRslt', align: 'center', width: 60, sortable: false, formatter: formatPrcssRslt},
|
||||
{name: 'prcssRsltCmnt', align: 'left', width: 300, sortable: false}
|
||||
],
|
||||
jsonReader: {repeatitems: false},
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: $("#container").height(),
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList: eval('[${rmsDefaultRowList}]'),
|
||||
ondblClickRow: function (rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var seqno = rowData['seqno'];
|
||||
var url2 = url_view + '?cmd=DETAIL';
|
||||
url2 += '&page=' + $(this).getGridParam("page");
|
||||
url2 += '&returnUrl=' + getReturnUrl();
|
||||
url2 += '&menuId=' + '${param.menuId}';
|
||||
url2 += '&seqno=' + seqno;
|
||||
url2 += '&' + getSearchUrl();
|
||||
goNav(url2);
|
||||
},
|
||||
loadError: function (jqXHR, textStatus, errorThrown) {
|
||||
var location = '<%=request.getContextPath()%>/';
|
||||
comloadError(jqXHR, textStatus, errorThrown, location);
|
||||
}
|
||||
});
|
||||
resizeJqGridWidth('grid', 'content_middle', '1000');
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "");
|
||||
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi, "");
|
||||
|
||||
if (start && end && start > end) {
|
||||
alert("조회기간 종료일은 시작일보다 커야합니다.");
|
||||
$("input[name=searchEndYYYYMMDD]").focus();
|
||||
return false;
|
||||
}
|
||||
$("input[name=searchStartDate]").val(start);
|
||||
$("input[name=searchEndDate]").val(end);
|
||||
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("input[name^=search]").keydown(function (key) {
|
||||
if (key.keyCode == 13) {
|
||||
$("#btn_search").click();
|
||||
}
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
</div>
|
||||
<div class="title">연동 배포 이력<span class="tooltip">IIM 레이아웃 연동 이력과 eCams 인터페이스 배포 이력을 조회합니다.</span></div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<colgroup>
|
||||
<col style="width:120px;"><col style="width:260px;">
|
||||
<col style="width:120px;"><col style="width:260px;">
|
||||
<col style="width:120px;"><col style="width:260px;">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>연동시간</th>
|
||||
<td>
|
||||
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:100px; border:1px solid #ebebec;">
|
||||
~
|
||||
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:100px; border:1px solid #ebebec;">
|
||||
<input type="hidden" name="searchStartDate" value="">
|
||||
<input type="hidden" name="searchEndDate" value="">
|
||||
</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>
|
||||
<td>
|
||||
<div class="select-style" style="display:inline-block; margin-left:6px;">
|
||||
<select name="searchPrcssRslt">
|
||||
<option value="">전체</option>
|
||||
<option value="S">성공</option>
|
||||
<option value="F">실패</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,141 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.succ-y { color:#1a73e8; font-weight:bold; }
|
||||
.succ-n { color:#d93025; font-weight:bold; }
|
||||
.pre-wrap { white-space:pre-wrap; word-break:break-all; height:500px; overflow: auto; line-height:1.5; }
|
||||
.recvdata-toolbar { text-align:right; margin-top: 5px; margin-bottom:6px; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/admin/rule/layoutSyncHistoryMan.json" />';
|
||||
var key = '${param.seqno}';
|
||||
var rawRecvData = '';
|
||||
var isRecvDataPretty = false;
|
||||
|
||||
function detail() {
|
||||
if (!key) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'DETAIL', seqno: key},
|
||||
success: function (data) {
|
||||
$('#seqno').text(data.seqno != null ? data.seqno : '');
|
||||
$('#serviceName').text(data.serviceName || '');
|
||||
$('#command').text(data.command || '');
|
||||
$('#loutName').text(data.loutName || '');
|
||||
$('#recvAmndHms').text(data.recvAmndHms || '');
|
||||
$('#prcssRsltCmnt').text(data.prcssRsltCmnt || '');
|
||||
|
||||
rawRecvData = data.recvData || '';
|
||||
isRecvDataPretty = false;
|
||||
$('#recvData').text(rawRecvData);
|
||||
|
||||
if (data.prcssRslt === 'S') {
|
||||
$('#prcssRslt').html('<span class="succ-y">성공</span>');
|
||||
} else if (data.prcssRslt === 'F') {
|
||||
$('#prcssRslt').html('<span class="succ-n">실패</span>');
|
||||
} else {
|
||||
$('#prcssRslt').text(data.prcssRslt || '');
|
||||
}
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
// JSON 지수표기(예: 1e+21) 값이 vkbeautify 처리 중 깨지지 않도록 문자열로 감쌌다가 되돌린다.
|
||||
function preserveExponential(jsonString) {
|
||||
var protectedJson = jsonString.replace(/:\s*([+-]?\d+\.?\d*e[+-]?\d+)/gi, ': "$1"');
|
||||
var prettyJson = vkbeautify.json(protectedJson);
|
||||
prettyJson = prettyJson.replace(/:\s*"([+-]?\d+\.?\d*e[+-]?\d+)"/gi, ': $1');
|
||||
return prettyJson;
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
|
||||
buttonControl();
|
||||
detail();
|
||||
|
||||
$('#btn_previous').click(function () { goNav(returnUrl); });
|
||||
|
||||
$('#btn_pretty_recvdata').click(function () {
|
||||
if (!rawRecvData) return;
|
||||
if (isRecvDataPretty) {
|
||||
$('#recvData').text(rawRecvData);
|
||||
isRecvDataPretty = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$('#recvData').text(preserveExponential(rawRecvData));
|
||||
isRecvDataPretty = true;
|
||||
} catch (e) {
|
||||
alert('JSON 형식이 아니거나 정렬할 수 없습니다.');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div>
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title" id="title">연동 배포 이력 상세</div>
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<td><span id="seqno"></span></td>
|
||||
<th>Service Name</th>
|
||||
<td><span id="serviceName"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>레이아웃명</th>
|
||||
<td><span id="loutName"></span></td>
|
||||
<th>Command</th>
|
||||
<td><span id="command"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>연동시간</th>
|
||||
<td><span id="recvAmndHms"></span></td>
|
||||
<th>결과</th>
|
||||
<td><span id="prcssRslt"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>결과메세지</th>
|
||||
<td colspan="3"><span id="prcssRsltCmnt"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>수신데이타</th>
|
||||
<td colspan="3">
|
||||
<div class="recvdata-toolbar">
|
||||
<button type="button" class="cssbtn" id="btn_pretty_recvdata"><i class="material-icons">format_align_left</i> 정렬</button>
|
||||
</div>
|
||||
<div id="recvData" class="pre-wrap"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -44,6 +44,9 @@
|
||||
.div-hover {
|
||||
background-color: #EDD5DD
|
||||
}
|
||||
|
||||
.btn_move { width:32px; min-width: 32px; height: 32px; background-color: #fff; border:1px solid #999; border-radius:4px; margin-bottom: 4px; }
|
||||
|
||||
|
||||
</style>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
@@ -888,7 +891,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
$("input[name=auto]").click(function(){
|
||||
$("#auto").click(function(){
|
||||
//debugger;
|
||||
jsPlumbInstance.deleteEveryConnection();
|
||||
|
||||
@@ -915,7 +918,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
$("input[name=addAll]").click(function() {
|
||||
$("#addAll").click(function() {
|
||||
console.debug('addAll');
|
||||
var srcRowId = sourceGrid.getGridParam( "selrow" );
|
||||
var tgtRowId = targetGrid.getGridParam( "selrow" );
|
||||
@@ -966,7 +969,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
$("input[name=removeAll]").click(function(){
|
||||
$("#removeAll").click(function(){
|
||||
var tgtRowId = targetGrid.getGridParam( "selrow" );
|
||||
if (tgtRowId == null) {alert("타겟레이아웃을 선택하여 주십시요.");return;}
|
||||
//grid data
|
||||
@@ -985,6 +988,49 @@
|
||||
jsPlumbInstance.deleteConnectionsForElement(document.getElementById(targetEndpointElementId));
|
||||
}
|
||||
});
|
||||
|
||||
$("#add").click(function() {
|
||||
|
||||
var srcRowId = sourceGrid.getGridParam( "selrow" );
|
||||
var tgtRowId = targetGrid.getGridParam( "selrow" );
|
||||
if (srcRowId == null) {alert('<%=localeMessage.getString("transformManDetail.alert1")%>');return;}
|
||||
if (tgtRowId == null) {alert('<%=localeMessage.getString("transformManDetail.alert2")%>');return;}
|
||||
|
||||
//field, ATTR 여부 판단
|
||||
var sourceRow = sourceGrid.getRowData( srcRowId );
|
||||
var targetRow = targetGrid.getRowData( tgtRowId );
|
||||
if ( (sourceRow["loutItemType"] == 'Field' || sourceRow["loutItemType"] == 'Attr' || sourceRow["loutItemType"] == 'Array') &&
|
||||
(targetRow["loutItemType"] == 'Field' || targetRow["loutItemType"] == 'Attr' || targetRow["loutItemType"] == 'Array') ){
|
||||
;
|
||||
}else{
|
||||
alert('<%=localeMessage.getString("transformManDetail.alert3")%>');
|
||||
return ;
|
||||
}
|
||||
|
||||
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNCMDNAME',addArrayTag(sourceRow['loutItemType'], addGroupTag(srcGroup,sourceRow['LOUTITEMPATH'])));
|
||||
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNRSULTITEMPATHNAME',addArrayTag(targetRow['loutItemType'], addGroupTag(tgtGroup,targetRow['LOUTITEMPATH'])));
|
||||
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNITEMSERNO',targetRow['loutItemSerno']);
|
||||
|
||||
try {
|
||||
jsPlumbConnect(srcRowId, tgtRowId);
|
||||
} catch (e) {console.error(e);}
|
||||
});
|
||||
|
||||
$("#remove").click(function(){
|
||||
var tgtRowId = targetGrid.getGridParam( "selrow" );
|
||||
if (tgtRowId == null) {alert("타겟레이아웃을 선택하여 주십시요.");return;}
|
||||
//grid data
|
||||
var targetData = targetGrid.getRowData();
|
||||
|
||||
targetGrid.jqGrid('setCell', tgtRowId, 'CNVSNCMDNAME', null);
|
||||
targetGrid.jqGrid('setCell', tgtRowId, 'CNVSNRSULTITEMPATHNAME', null);
|
||||
targetGrid.jqGrid('setCell', tgtRowId, 'CNVSNITEMSERNO', null);
|
||||
targetGrid.jqGrid('setCell', tgtRowId, 'CNVSNITEMBASCVAL', null);
|
||||
|
||||
const targetEndpointElementId = targetGrid.getEndpointElementId(tgtRowId);
|
||||
jsPlumbInstance.deleteConnectionsForElement(document.getElementById(targetEndpointElementId));
|
||||
|
||||
});
|
||||
|
||||
$("textarea[name=CNVSNRSULTITEMPATHNAME],textarea[name=CNVSNCMDNAME],input[name=CNVSNITEMBASCVAL],input[name=CNVSNITEMSERNO]").blur(function(){
|
||||
var rowId = targetGrid.getGridParam( "selrow" );
|
||||
@@ -2161,12 +2207,12 @@
|
||||
<div id="sourcePage"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:30%; text-align:center; vertical-align: top;">
|
||||
|
||||
<br /><!-- 이 br 이 없을 때 버튼이 제대로 동작 하지 않는다. -->
|
||||
<input type=button id="removeAll" name="removeAll" class="btn_img btn_removeAll"/><br />
|
||||
<input type=button id="addAll" name="addAll" class="btn_img btn_addAll"/><br />
|
||||
<input type=button id="auto" name="auto" class="btn_img btn_auto" />
|
||||
<td style="width:30%; min-width:70px; text-align:center; vertical-align: top; ">
|
||||
<button type=button id="remove" name="remove" class="cssbtn btn_move" title="remove"><</button>
|
||||
<button type=button id="add" name="add" class="cssbtn btn_move" title="add"> > </button><br />
|
||||
<button type=button id="removeAll" name="removeAll" class="cssbtn btn_move" title="remove all"> << </button>
|
||||
<button type=button id="addAll" name="addAll" class="cssbtn btn_move" title="add all"> >> </button><br />
|
||||
<button type=button id="auto" name="auto" class="cssbtn btn_move" style="width: 70px; " title="auto"> Auto </button>
|
||||
</td>
|
||||
<td style="vertical-align:top;">
|
||||
<table class="table_row" cellspacing="0">
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%@ page import="com.eactive.eai.common.util.SystemUtil"%>
|
||||
<%@ page import="com.eactive.eai.rms.common.context.MonitoringContext"%>
|
||||
<%@ page import="com.eactive.eai.rms.common.util.CommonUtil"%>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
MonitoringContext monitoringContext = (MonitoringContext)CommonUtil.getBean(request, "monitoringContext");
|
||||
String deployYnStr = monitoringContext.getStringProperty(MonitoringContext.IOMAP_DEPLOY_YN_BY_RMS, "N");
|
||||
boolean deployYn = "Y".equalsIgnoreCase(deployYnStr);
|
||||
String adminOnlyYnStr = monitoringContext.getStringProperty(MonitoringContext.IOMAP_DEPLOY_YN_BY_ADMIN_ONLY, "Y");
|
||||
boolean adminOnly = "Y".equalsIgnoreCase(adminOnlyYnStr);
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
@@ -17,62 +24,48 @@
|
||||
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<script language="javascript" >
|
||||
<script>
|
||||
var url = '<c:url value="/onl/admin/rule/transformMan.json" />';
|
||||
var url_view = '<c:url value="/onl/admin/rule/transformMan.view" />';
|
||||
var url_excel= '<c:url value="/onl/admin/rule/transformMan.excel"/>';
|
||||
var roleString = "<%=SessionManager.getRoleIdString(request)%>";
|
||||
var devSvr = <%=SystemUtil.isDevServer()%>;
|
||||
|
||||
function dateFormater(cellvalue, options, rowObject){
|
||||
if (cellvalue==null|| cellvalue==undefined) return "";
|
||||
if (cellvalue.trim().length < 14) return "";
|
||||
var display = "";
|
||||
display=display+cellvalue.substr(0,4)+"-";
|
||||
display=display+cellvalue.substr(4,2)+"-";
|
||||
display=display+cellvalue.substr(6,2)+" ";
|
||||
|
||||
display=display+cellvalue.substr(8,2)+":";
|
||||
display=display+cellvalue.substr(10,2)+":";
|
||||
display=display+cellvalue.substr(12,2);
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
var stageSvr = <%=SystemUtil.isStagingServer()%>;
|
||||
var adminOnly = <%=adminOnly%>;
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
|
||||
$('#grid').jqGrid({
|
||||
datatype:"json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData : { cmd : 'LIST'
|
||||
, searchBizCode : $('input[name=searchBizCode]').val()
|
||||
, searchCnvsnName : $('input[name=searchCnvsnName]').val()
|
||||
, searchCnvsnDesc : $('input[name=searchCnvsnDesc]').val()
|
||||
|
||||
},
|
||||
postData : postData,
|
||||
colNames:[
|
||||
'<%=localeMessage.getString("transformMan.eaibzwkdstcd")%>',
|
||||
'<%=localeMessage.getString("transformMan.telCngMappCd")%>',
|
||||
'<%=localeMessage.getString("transformMan.telCngMappDesc")%>',
|
||||
'<%=localeMessage.getString("transformMan.ifSvcCd")%>',
|
||||
'<%=localeMessage.getString("transformMan.ifSvcDesc")%>',
|
||||
'<%=localeMessage.getString("transformMan.eaiSvrDiv")%>',
|
||||
'<%= localeMessage.getString("combo.useYn")%> ',
|
||||
'<%=localeMessage.getString("transformMan.modifyDate")%>',
|
||||
'<%=localeMessage.getString("transformMan.verInfo")%>',
|
||||
'타입',
|
||||
'cnvsnType',
|
||||
'flag'
|
||||
],
|
||||
colModel:[
|
||||
{ name : 'EAIBZWKDSTCD' , align:'center' ,sortable:false , width : "40"},
|
||||
{ name : 'CNVSNNAME' , align:'left' },
|
||||
{ name : 'EAISVCNAME' , align:'left' },
|
||||
{ name : 'EAISVCDESC' , align:'left' },
|
||||
{ name : 'EAIBZWKDSTCD' , align:'center' ,sortable:false , width:"30"},
|
||||
{ name : 'CNVSNNAME' , align:'left' , width:"100" },
|
||||
{ name : 'CNVSNDESC' , align:'left' , width:"100" },
|
||||
{ name : 'EAISVCNAME' , align:'left' , width:"70" },
|
||||
{ name : 'EAISVCDESC' , align:'left' , width:"100"},
|
||||
{ name : 'EAISEVRDSTCD' , hidden : true },
|
||||
{ name : 'USEYN', align:'center', width:30,editoptions:{value:"0:<%= localeMessage.getString("combo.usen")%>;1:<%= localeMessage.getString("combo.usey")%>"}, formatter:"select"},
|
||||
{ name : 'LASTAMNDHMS' , align:'center' , formatter: dateFormater, width : "70" },
|
||||
{ name : 'VERINFO' , align:'center' , width : "70" },
|
||||
{ name : 'CNVSNTYPE' , hidden : true },
|
||||
{ name : 'USEYN', align:'center', width:30 ,editoptions:{value:"0:<%= localeMessage.getString("combo.usen")%>;1:<%= localeMessage.getString("combo.usey")%>"}, formatter:"select"},
|
||||
{ name : 'LASTAMNDHMS' , align:'center' , width : "60" },
|
||||
{ name : 'VERINFO' , align:'center' , width : "30" },
|
||||
{ name : 'CNVSNTYPE' , hidden : true },
|
||||
{ name : 'FLAG' , hidden : true }
|
||||
],
|
||||
jsonReader: {
|
||||
@@ -136,7 +129,8 @@ $(document).ready(function() {
|
||||
//key값
|
||||
url2 += '&cnvsnName='+cnvsnName;
|
||||
url2 += '&eaiSvcName='+eaiSvcName;
|
||||
url2 += '&eaiSvcDesc='+eaiSvcDesc;
|
||||
//url2 += '&eaiSvcDesc='+eaiSvcDesc;
|
||||
url2 += '&eaiSvcDesc='+encodeURIComponent(eaiSvcDesc);
|
||||
url2 += '&cnvsnDesc='+cnvsnDesc;
|
||||
url2 += '&eaiSevrDStcd='+eaiSevrDStcd;
|
||||
url2 += '&useYn='+useYn;
|
||||
@@ -161,7 +155,6 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
@@ -217,7 +210,7 @@ $(document).ready(function() {
|
||||
}
|
||||
return false;
|
||||
}else{
|
||||
if(confirm("초기화 하시겠습니까?")){
|
||||
if(confirm("<%=localeMessage.getString("eaiMessage.confirm2")%>")){
|
||||
var postData = [];
|
||||
postData.push({ name: "cmd" , value:"LIST_INITIALIZE"});
|
||||
postData.push({ name: "chkItems" , value : chkArr});
|
||||
@@ -242,8 +235,13 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
$('#btn_cmsave').click(onclickBtnCmSave);
|
||||
$('#btn_clone').click(onclickBtnClone);
|
||||
|
||||
$("#btn_delete").click(function(){
|
||||
if(!confirm("삭제하시겠습니까?")){
|
||||
return;
|
||||
}
|
||||
|
||||
var id = "grid";
|
||||
var cnvsnNameArr = new Array();
|
||||
var eaiSvcNameArr = new Array();
|
||||
@@ -263,7 +261,7 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
if(chkcnt == 0){
|
||||
alert("변환매핑을 선택해 주세요");
|
||||
alert("<%= localeMessage.getString("standardLayout.message4") %>");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -303,10 +301,18 @@ $(document).ready(function() {
|
||||
});
|
||||
buttonControl();
|
||||
setBtnHide(roleString, "admin", "btn_initialize");
|
||||
//setBtnControl(roleString, "admin", "btn_cmsave");
|
||||
if(!devSvr){
|
||||
$("#btn_cmsave").hide();
|
||||
|
||||
if(devSvr || stageSvr){
|
||||
if (adminOnly) {
|
||||
setBtnHide(roleString, "admin", "btn_clone");
|
||||
}
|
||||
}else{
|
||||
$("#btn_clone").hide();
|
||||
}
|
||||
//setBtnControl(roleString, "admin", "btn_cmsave");
|
||||
//if(!devSvr){
|
||||
// $("#btn_cmsave").hide();
|
||||
//}
|
||||
});
|
||||
|
||||
function setBtnControl(roleString, condiVal, compoId)
|
||||
@@ -429,6 +435,62 @@ function addCMResource(eaiBzwkDsdCd, obj, cnvsnNames) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onclickBtnClone() {
|
||||
var sel = $('#grid').jqGrid('getGridParam', 'selarrrow');
|
||||
if (sel.length == 0) {
|
||||
alert('<%= localeMessage.getString("eaiMessage.alert17")%>');
|
||||
return;
|
||||
}
|
||||
var selectedTransformNames = [];
|
||||
|
||||
$(sel).each(function(index, i) {
|
||||
var cnvsnName = $('#grid').getCell(i, 'CNVSNNAME');
|
||||
selectedTransformNames.push(cnvsnName);
|
||||
});
|
||||
|
||||
var url2 = url_view;
|
||||
url2 += "?cmd=POPUP CALENDAR";
|
||||
var args = new Object();
|
||||
args['selectedCnvsnName'] = selectedTransformNames.join(',');
|
||||
showModal(url2,args,540,270, function(arg){
|
||||
var args = null;
|
||||
if(arg == null || arg == undefined ) {
|
||||
//chrome
|
||||
args = this.dialogArguments;
|
||||
args.returnValue = this.returnValue;
|
||||
} else {
|
||||
//ie
|
||||
args = arg;
|
||||
}
|
||||
if( !args || !args.returnValue ) return;
|
||||
deployTransforms(args.returnValue);
|
||||
});
|
||||
}
|
||||
|
||||
function deployTransforms(ret) {
|
||||
var params = ret;
|
||||
params['cmd'] = 'CLONETOPROD';
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
dataType:"json",
|
||||
data:params,
|
||||
success:function(json){
|
||||
alert(json.message);
|
||||
list($("#grid").getGridParam("page"));
|
||||
},
|
||||
error:function(xhr,status,error){
|
||||
if (xhr.responseText != null) {
|
||||
try {
|
||||
alert(JSON.parse(xhr.responseText).errorMsg);
|
||||
} catch (e) {
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
@@ -441,6 +503,11 @@ function addCMResource(eaiBzwkDsdCd, obj, cnvsnNames) {
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<% if (SystemUtil.isDevServer() && deployYn) { %>
|
||||
<button type="button" class="cssbtn" id="btn_clone" level="W"><i class="material-icons">move_up</i> <%= localeMessage.getString("button.copyStage") %></button>
|
||||
<% } else if (SystemUtil.isStagingServer() && deployYn) { %>
|
||||
<button type="button" class="cssbtn" id="btn_clone" level="W"><i class="material-icons">move_up</i> <%= localeMessage.getString("button.copyProd") %></button>
|
||||
<% } %>
|
||||
<button type="button" class="cssbtn" id="btn_excel" level="W" status="DETAIL"><i class="material-icons">table_view</i> <%= localeMessage.getString("button.excel") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_initialize" level="W" ><i class="material-icons">restart_alt</i> <%= localeMessage.getString("button.reset") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL,NEW"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
|
||||
@@ -469,9 +536,13 @@ function addCMResource(eaiBzwkDsdCd, obj, cnvsnNames) {
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:180px;"><%=localeMessage.getString("transformMan.telCngMappDesc")%> </th>
|
||||
<td colspan="3">
|
||||
<td>
|
||||
<input type="text" name="searchCnvsnDesc" value="${param.searchCnvsnDesc}">
|
||||
</td>
|
||||
<th style="width:180px;"><%=localeMessage.getString("transformMan.ifSvcCd")%> </th>
|
||||
<td>
|
||||
<input type="text" name="searchIfSvcCd" value="${param.searchIfSvcCd}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<%@ page import="java.io.*"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<script language="javascript" >
|
||||
var url = '<c:url value="/onl/transaction/apim/apiInterfaceMan.json"/>';
|
||||
var url_view = '<c:url value="/onl/transaction/apim/apiInterfaceMan.view"/>';
|
||||
|
||||
var selectName = "searchEaiBzwkDstcd"; // selectBox Name
|
||||
function processSelectedData() {
|
||||
var grid = $("#grid");
|
||||
var selectedRowIds = grid.jqGrid('getGridParam', 'selarrrow');
|
||||
var selectedData = [];
|
||||
|
||||
for(var i = 0; i < selectedRowIds.length; i++) {
|
||||
var rowData = grid.jqGrid('getRowData', selectedRowIds[i]);
|
||||
selectedData.push({
|
||||
bizCode: rowData.eaiBzwkDstcd, // CLINET
|
||||
apiId: rowData.eaiSvcName,
|
||||
apiDesc: rowData.eaiSvcDesc, // SCOPE-API
|
||||
APIFULLPATH: rowData.apiFullPath,
|
||||
BZWKSVCKEYNAME: rowData.bzwksvckeyname
|
||||
});
|
||||
}
|
||||
|
||||
if(selectedData.length === 0) {
|
||||
alert("선택된 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
alert("선택한 " + selectedData.length + "건의 API를 추가합니다.");
|
||||
window.returnValue = selectedData.length === 1 ? selectedData[0] : selectedData;
|
||||
window.close();
|
||||
}
|
||||
|
||||
function updateSelectionInfo(gridId) {
|
||||
var grid = $(gridId || "#grid");
|
||||
var selectedRows = grid.jqGrid('getGridParam', 'selarrrow');
|
||||
$("#selected-count").text("총: " + selectedRows.length + " 건");
|
||||
}
|
||||
|
||||
function init( callback) {
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
dataType:"json",
|
||||
data:{cmd: 'LIST_COMBO'},
|
||||
success:function(json){
|
||||
new makeOptions("BIZCODE","BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("","전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering();
|
||||
|
||||
setSearchable(selectName); // 콤보에 searchable 설정
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
function detail(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
$("#grid").setGridParam({ url:url,postData: postData }).trigger("reloadGrid");
|
||||
}
|
||||
|
||||
function search(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
|
||||
}
|
||||
function ioFormatter(cellvalue,options,rowObject){
|
||||
var serviceType = sessionStorage["serviceType"];
|
||||
if(cellvalue == "I"){
|
||||
return "타발";
|
||||
}else if(cellvalue =="O"){
|
||||
return "당발";
|
||||
}
|
||||
}
|
||||
function adapterNameShortFormatter(cellvalue,options,rowObject){
|
||||
if(cellvalue == null || cellvalue == '')
|
||||
return ''
|
||||
return cellvalue.substring(1, 4);
|
||||
}
|
||||
|
||||
function list(){
|
||||
detail()
|
||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
// gridPostData.searchApiEnableYn = 'Y';
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var selectedApiIds = urlParams.get("selectedApiIds");
|
||||
$('#grid').jqGrid({
|
||||
datatype : "json",
|
||||
mtype : 'POST',
|
||||
url : url,
|
||||
postData : gridPostData,
|
||||
colNames : [ '업무구분',
|
||||
'<%= localeMessage.getString("eaiMessage.eaiSvcName")%>',
|
||||
'<%= localeMessage.getString("eaiMessage.eaiSvcDesc")%>',
|
||||
'API FULL PATH',
|
||||
'업무서비스명',
|
||||
'요청',
|
||||
'응답',
|
||||
'작성자',
|
||||
'가상응답여부',
|
||||
'SyncAsyncType',
|
||||
],
|
||||
colModel : [ { name : 'eaiBzwkDstcd' , align : 'center' , width:'40', sortable:false},
|
||||
{ name : 'eaiSvcName' , align : 'left' , width:'100'},
|
||||
{ name : 'eaiSvcDesc' , align : 'left' },
|
||||
{ name : 'apiFullPath' , align : 'left' , width:'100'},
|
||||
{ name : 'bzwksvckeyname' , align : 'left' , width:'100', hidden: true},
|
||||
{ name : 'fromAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
|
||||
{ name : 'toAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
|
||||
{ name : 'author' , align : 'center' , width:'60' },
|
||||
{ name : 'simYn' , align : 'center' , width:'40' },
|
||||
{ name : 'syncAsyncType' , align : 'center' , width:'40', hidden: true},
|
||||
],
|
||||
jsonReader : {
|
||||
repeatitems : false
|
||||
},
|
||||
pager : $('#pager'),
|
||||
page : '${param.page}',
|
||||
rowNum : '${rmsDefaultRowNum}',
|
||||
autoheight : true,
|
||||
height : $("#container").height(),
|
||||
autowidth : true,
|
||||
viewrecords : true,
|
||||
multiselect: false,
|
||||
multiboxonly: false,
|
||||
rowList : eval('[${rmsDefaultRowList}]'),
|
||||
loadComplete:function (d){
|
||||
var $grid = $(this);
|
||||
if(selectedApiIds) {
|
||||
const selectedIds = selectedApiIds.split(',');
|
||||
$.each($grid.getDataIDs(), function(_, id) {
|
||||
var rowData = $grid.getRowData(id);
|
||||
if(selectedIds.includes(rowData.eaiSvcName)) {
|
||||
$grid.jqGrid('setSelection', id, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for(var i = 0 ; i< colModel.length; i++){
|
||||
$(this).setColProp(colModel[i].name, {sortable : false});
|
||||
}
|
||||
updateSelectionInfo("#grid");
|
||||
},
|
||||
onSelectRow: function(rowid, status, e) {
|
||||
updateSelectionInfo("#grid");
|
||||
},
|
||||
onSelectAll: function(rowids, status) {
|
||||
updateSelectionInfo("#grid");
|
||||
},
|
||||
ondblClickRow : function(rowId) {
|
||||
const rowData = $(this).getRowData(rowId);
|
||||
const returnValue = {
|
||||
bizCode: rowData.eaiBzwkDstcd,
|
||||
apiId: rowData.eaiSvcName,
|
||||
apiDesc: rowData.eaiSvcDesc
|
||||
};
|
||||
//alert("선택한 API를 추가합니다.\nAPI ID: " + rowData.eaiSvcName);
|
||||
window.returnValue = returnValue;
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
init(list);
|
||||
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
// 업무구분명 선택 시 자동 검색
|
||||
$("select[name=searchEaiBzwkDstcd]").change(function() {
|
||||
search();
|
||||
});
|
||||
$("#btn_search").click(function(){
|
||||
search();
|
||||
});
|
||||
$("input[name^=search]").keydown(function(key){
|
||||
if (key.keyCode == 13){
|
||||
$("#btn_search").click();
|
||||
}
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
|
||||
$("#btn_select").click(function(){
|
||||
processSelectedData();
|
||||
});
|
||||
|
||||
$("#btn_close").click(function () {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<div class="content_middle" id="content_middle" style="margin-top: 0px">
|
||||
<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_select" level="R" ><i class="material-icons">check</i> <%= localeMessage.getString("button.check") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_close" level="R" status="DETAIL"><i class="material-icons">close</i> <%= localeMessage.getString("button.close") %></button>
|
||||
</div>
|
||||
<div class="title" id="title" >${rmsMenuName}</div>
|
||||
<!-- 선택 정보 표시 영역 추가 -->
|
||||
<div style="margin-bottom: 5px;">
|
||||
<span id="selected-count" style="color: #666; font-size: 12px;"></span>
|
||||
</div>
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:180px;">업무구분명</th>
|
||||
<td>
|
||||
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}">
|
||||
</select>
|
||||
</td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}">
|
||||
</td>
|
||||
<th style="width:180px;">API FULL PATH</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table id="grid" ></table>
|
||||
<div id="pager"></div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -46,10 +46,10 @@ $(document).ready(function() {
|
||||
, src_layout : src_layout
|
||||
},
|
||||
colNames:[
|
||||
'<%=localeMessage.getString("tranManDtPop.conRsItPtNm")%>',
|
||||
'<%=localeMessage.getString("tranManDtPop.conOrd")%>',
|
||||
'<%=localeMessage.getString("tranManDtPop.ItDft")%>',
|
||||
'<%=localeMessage.getString("tranManDtPop.itSrlNmb")%>',
|
||||
'변환결과 항목경로명',
|
||||
'변환명령',
|
||||
'항목 기본값',
|
||||
'항목 일련번호',
|
||||
'타겟Flag',
|
||||
'소스Flag',
|
||||
'<%=localeMessage.getString("tranManDtPop.errMsg")%>'
|
||||
@@ -104,7 +104,6 @@ $(document).ready(function() {
|
||||
resizeJqGridWidth('grid','title','1000');
|
||||
|
||||
$("#btn_close").click(function(){
|
||||
//debugger;
|
||||
window.close();
|
||||
});
|
||||
$("body").css("overflow-x","hidden");
|
||||
@@ -115,13 +114,12 @@ $(document).ready(function() {
|
||||
<body>
|
||||
<div class="popup_box" id="title">
|
||||
<div class="search_wrap">
|
||||
<%-- <img id="btn_close" src="<c:url value="/img/btn_close.png"/>" level="R"/> --%>
|
||||
<button type="button" class="cssbtn" id="btn_close" level="R"><i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %></button>
|
||||
<img id="btn_close" src="<c:url value="/img/btn_close.png"/>" level="R"/>
|
||||
</div>
|
||||
<div class="title"><%=localeMessage.getString("tranManDtPop.title")%></div>
|
||||
<div class="title">변환매핑항목 상세</div>
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<tr>
|
||||
<th style="width:180px;"><%=localeMessage.getString("tranManDtPop.conMapCd")%></th>
|
||||
<th style="width:180px;">변환매핑 코드</th>
|
||||
<td>
|
||||
<input type="text" name="cnvsnName" readonly>
|
||||
</td>
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
|
||||
<script language="javascript" >
|
||||
<script language="javascript" >
|
||||
var url_view = '<c:url value="/onl/admin/rule/transformMan.view" />';
|
||||
var $ = jQuery.noConflict();
|
||||
var bzwkDstCd = "";
|
||||
|
||||
@@ -81,8 +82,9 @@ $(document).ready(function() {
|
||||
var key = "";
|
||||
var args = new Object();
|
||||
args['eaiSvcName'] = $('input[name=eaiSvcName]').val();
|
||||
var url='<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
||||
url = url + "?cmd=POPUP";
|
||||
var url=url_view; //'<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
||||
url = url + "?cmd=API_POPUP";
|
||||
console.log('[new]', url);
|
||||
var ret = showModal(url,args,1020,630, function(arg){
|
||||
var args = null;
|
||||
if(arg == null || arg == undefined ) {//chrome
|
||||
@@ -96,15 +98,15 @@ $(document).ready(function() {
|
||||
|
||||
var ret = args.returnValue;
|
||||
console.log("ret",ret);
|
||||
key = ret['key'];
|
||||
key = ret['apiId'];
|
||||
|
||||
$("input[name=eaiSvcName]").val(key);
|
||||
$("input[name=eaiSvcDesc]").val(ret['eaiSvcDesc']);
|
||||
bzwkDstCd = ret['eaiBzwkDstCd'];
|
||||
$("input[name=eaiSvcDesc]").val(ret['apiDesc']);
|
||||
bzwkDstCd = ret['bizCode'];
|
||||
|
||||
$("input[name=eaiSvcName]").change();
|
||||
$("select[name=eaiSevrDstcd]").val(ret['eaiSevrDstcd']);
|
||||
$("select[name=eaiSevrDstcd]").attr("disabled",true);
|
||||
//$("select[name=eaiSevrDstcd]").val(ret['eaiSevrDstcd']);
|
||||
//$("select[name=eaiSevrDstcd]").attr("disabled",true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,7 +146,7 @@ $(document).ready(function() {
|
||||
</ul>
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_regist" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.register") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
<%-- <img src="<c:url value="/img/btn_regist.png"/>" alt="" id="btn_regist" level="W" status="DETAIL,NEW" /> --%>
|
||||
@@ -159,25 +161,29 @@ $(document).ready(function() {
|
||||
<form id="ajaxForm">
|
||||
<table class="table_row" cellspacing="0">
|
||||
<tr>
|
||||
<th style="width:25%;"><%=localeMessage.getString("transformMan.ifSvcCd")%> </th>
|
||||
<td colspan="3">
|
||||
<input type="text" name="eaiSvcName" style="width:calc(100% - 60px);" readonly> <%-- <img id="btn_search" src="<c:url value="/img/btn_pop_search.png" />" level="R" class="btn_img" /> --%>
|
||||
<th style="width:14%; min-width:130px;"><%=localeMessage.getString("transformMan.ifSvcCd")%> </th>
|
||||
<td style="width:31%;">
|
||||
<input type="text" name="eaiSvcName" style="width:60%;" readonly> <!-- <img id="btn_search" src="<c:url value="/img/btn_pop_search.png" />" level="R" class="btn_img" /> -->
|
||||
<button type="button" class="cssbtn smallBtn2" id="btn_search" style="vertical-align:middle; font-weight:bold;" level="R"><i class="material-icons">search</i><%= localeMessage.getString("button.search") %></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%=localeMessage.getString("transformMan.ifSvcDesc")%></th>
|
||||
<td colspan="3">
|
||||
</td>
|
||||
<th style="width:14%; min-width:130px;"><%=localeMessage.getString("transformMan.ifSvcDesc")%></th>
|
||||
<td style="width:41%;">
|
||||
<input type="text" name="eaiSvcDesc" readOnly>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%=localeMessage.getString("transformMan.eaiSvrDiv")%></th>
|
||||
<td colspan="3">
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="eaiSevrDstcd" />
|
||||
</div>
|
||||
</td>
|
||||
<th style="width:13%; min-width:130px;"><%=localeMessage.getString("combo.useYn")%></th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="useYn" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%=localeMessage.getString("transformMan.cnvsnType")%></th>
|
||||
@@ -186,20 +192,13 @@ $(document).ready(function() {
|
||||
<select name="cnvsnType" />
|
||||
</div>
|
||||
</td>
|
||||
<th><%=localeMessage.getString("combo.useYn")%></th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="useYn" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
<th><%=localeMessage.getString("transformMan.telCngMappCd")%></th>
|
||||
<td colspan="3"><input type="text" name="cnvsnName" readonly="readonly"/></td>
|
||||
<td><input type="text" name="cnvsnName" readonly="readonly"/></td>
|
||||
</tr>
|
||||
|
||||
<tr height="100px">
|
||||
<th><%=localeMessage.getString("transformMan.cnvsnDesc")%></th><td colspan="3"><textarea name="cnvsnDesc" style="width:100%;height:100px"></textarea></td>
|
||||
<tr>
|
||||
<th><%=localeMessage.getString("transformMan.cnvsnDesc")%></th><td colspan="3"><textarea name="cnvsnDesc" style="width:100%;height:50px"></textarea></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
@@ -47,9 +47,6 @@ function init(callback){
|
||||
dataType:"json",
|
||||
data:{cmd: 'LIST_INIT', codeGroupArr : 'SVC_SAME_TIME_TYPE,<%= tranTypeCode %>'},
|
||||
success:function(json){
|
||||
new makeOptions("CODE","NAME").setObj($("select[name=searchSvcMotivUsedStcd]")).setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>").setData(json.list[0].SVC_SAME_TIME_TYPE).rendering();
|
||||
new makeOptions("CODE","NAME").setObj($("select[name=searchPsvintfacdsticName]")).setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>").setData(json.list[0].SVC_SAME_TIME_TYPE).rendering();
|
||||
new makeOptions("CODE","NAME").setObj($("select[name=searchFlowControl]")).setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>").setData(json.listFlowControl).rendering();
|
||||
new makeOptions("CODE","NAME").setObj($("select[name=searchTranType]")).setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>").setData(json.list[0].<%= tranTypeCode %>).rendering();
|
||||
svrInst = json.listInoutType;
|
||||
putSelectFromParam();
|
||||
@@ -89,8 +86,6 @@ $(document).ready(function() {
|
||||
gridPostData["rmsMenuAuth"]= auth;
|
||||
|
||||
init(detail);
|
||||
gridPostData["searchSvcMotivUsedStcd"] = '${param.searchSvcMotivUsedStcd}';
|
||||
gridPostData["searchPsvintfacdsticName"] = '${param.searchPsvintfacdsticName}';
|
||||
|
||||
$('#grid').jqGrid({
|
||||
datatype:"json",
|
||||
@@ -100,10 +95,8 @@ $(document).ready(function() {
|
||||
colNames:['<%= localeMessage.getString("eaiMsg.bizCode")%> ',
|
||||
'<%= localeMessage.getString("eaiMessage.eaiSvcName")%> ',
|
||||
'<%= localeMessage.getString("eaiMessage.eaiSvcDesc")%> ',
|
||||
'<%= localeMessage.getString("eaiMessage.svcMotivUsedStcd")%>',
|
||||
'<%= localeMessage.getString("eaiMessage.psvintfacdsticName")%>',
|
||||
'<%= localeMessage.getString("eaiMessage.queueName")%> ',
|
||||
'<%= localeMessage.getString("eaiMessage.flowCtrlRoutName")%> ',
|
||||
'<%= localeMessage.getString("eaiMessage.gstatSysAdptr")%>',
|
||||
'<%= localeMessage.getString("eaiMessage.psvSysAdptr")%>',
|
||||
'<%= localeMessage.getString("eaiMessage.tranType")%> ',
|
||||
'<%= localeMessage.getString("combo.useYn")%> ',
|
||||
'<%= localeMessage.getString("eaiMessage.sevrDstCd")%>',
|
||||
@@ -113,15 +106,13 @@ $(document).ready(function() {
|
||||
{ name : 'EAIBZWKDSTCD' , align:'center' ,sortable:false, width:20 },
|
||||
{ name : 'EAISVCNAME' , align:'left', width:50 },
|
||||
{ name : 'EAISVCDESC' , align:'left' },
|
||||
{ name : 'SVCMOTIVUSEDSTCD', align:'center', width:50 },
|
||||
{ name : 'PSVINTFACDSTICNAME', align:'center', width:50 },
|
||||
{ name : 'NONMOTIVROUTURINAME' , align:'left', width:90 },
|
||||
{ name : 'FLOWCTRLROUTNAME' , align:'center', width:40 },
|
||||
{ name : 'GSTATSYSADPTRBZWKGROUPNAME', align:'left', width:80 },
|
||||
{ name : 'PSVSYSADPTRBZWKGROUPNAME', align:'left', width:80 },
|
||||
{ name : 'TRANTYPE', align:'center', width:30 },
|
||||
{ name : 'USEYN', align:'center', width:30,editoptions:{value:"0:<%= localeMessage.getString("combo.usen")%>;1:<%= localeMessage.getString("combo.usey")%>"}, formatter:"select"},
|
||||
{ name : 'EAISEVRDSTCD', align:'center', width:30 , formatter:formatter, hidden:true},
|
||||
{ name : 'VERINFO', align:'center', width:30 }
|
||||
|
||||
|
||||
],
|
||||
jsonReader: {
|
||||
repeatitems:false
|
||||
@@ -426,28 +417,25 @@ function deployInterfaces(ret) {
|
||||
<td>
|
||||
<input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}">
|
||||
</td>
|
||||
<th style="width:10%; min-width:120px;"><%= localeMessage.getString("eaiMessage.svcMotivUsedStcd")%></th>
|
||||
<td><div class="select-style"><select name="searchSvcMotivUsedStcd" value="${param.searchSvcMotivUsedStcd}"></select></div></td>
|
||||
<th style="width:10%; min-width:90px;"><%= localeMessage.getString("eaiMessage.tranType")%></th>
|
||||
<td><div class="select-style"><select name="searchTranType" value="${param.searchTranType}"></select></div></td>
|
||||
|
||||
<th style="width:10%; min-width:90px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}">
|
||||
</td>
|
||||
</tr>
|
||||
<th style="width:10%; min-width:90px;"><%= localeMessage.getString("eaiMessage.tranType")%></th>
|
||||
<td><div class="select-style"><select name="searchTranType" value="${param.searchTranType}"></select></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("eaiMessage.searchBzwkDstCd")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}">
|
||||
</td>
|
||||
<th><%= localeMessage.getString("eaiMessage.psvintfacdsticName")%></th>
|
||||
<td><div class="select-style"><select name="searchPsvintfacdsticName" value="${param.searchPsvintfacdsticName}"></select></div></td>
|
||||
<th><%= localeMessage.getString("eaiMessage.searchFlowControl")%></th>
|
||||
<td><div class="select-style"><select name="searchFlowControl" value="${param.searchFlowControl}"></select></div></td>
|
||||
<th><%= localeMessage.getString("eaiMessage.queueName")%></th>
|
||||
<th><%= localeMessage.getString("eaiMessage.gstatSysAdptr")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchQueueName" value="${param.searchQueueName}">
|
||||
<input type="text" name="searchGstatSysAdptrBzwkGroupName" value="${param.searchGstatSysAdptrBzwkGroupName}">
|
||||
</td>
|
||||
<th><%= localeMessage.getString("eaiMessage.psvSysAdptr")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchPsvSysAdptrBzwkGroupName" value="${param.searchPsvSysAdptrBzwkGroupName}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -72,13 +72,22 @@
|
||||
});
|
||||
|
||||
}
|
||||
function detail(){
|
||||
// 체크박스는 name 을 search* 로 두면 getSearchForJqgrid 가 해제 상태에서도 value 를 담아버린다
|
||||
// (input[type!=radio][name^=search] 를 checked 여부와 무관하게 수집) → 여기서 직접 붙인다
|
||||
function getPopupSearchData() {
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
postData.searchApiSpecYn = $("#chkApiSpec").is(":checked") ? "Y" : "";
|
||||
postData.searchPortalDisplayYn = $("#chkPortalDisplay").is(":checked") ? "Y" : "";
|
||||
return postData;
|
||||
}
|
||||
|
||||
function detail(){
|
||||
var postData = getPopupSearchData();
|
||||
$("#grid").setGridParam({ url:url,postData: postData }).trigger("reloadGrid");
|
||||
}
|
||||
|
||||
function search(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
var postData = getPopupSearchData();
|
||||
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
|
||||
}
|
||||
function ioFormatter(cellvalue,options,rowObject){
|
||||
@@ -97,7 +106,7 @@
|
||||
|
||||
function list(){
|
||||
detail()
|
||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
var gridPostData = getPopupSearchData();
|
||||
// gridPostData.searchApiEnableYn = 'Y';
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var selectedApiIds = urlParams.get("selectedApiIds");
|
||||
@@ -116,21 +125,27 @@
|
||||
'작성자',
|
||||
'가상응답여부',
|
||||
'SyncAsyncType',
|
||||
'변경일시',
|
||||
],
|
||||
colModel : [ { name : 'eaiBzwkDstcd' , align : 'center' , width:'40', sortable:false},
|
||||
{ name : 'eaiSvcName' , align : 'left' , width:'100'},
|
||||
{ name : 'eaiSvcDesc' , align : 'left' },
|
||||
{ name : 'apiFullPath' , align : 'left' , width:'100'},
|
||||
{ name : 'eaiSvcName' , align : 'left' , width:'100', sortable:true},
|
||||
{ name : 'eaiSvcDesc' , align : 'left' , sortable:false},
|
||||
{ name : 'apiFullPath' , align : 'left' , width:'100', sortable:false},
|
||||
{ name : 'bzwksvckeyname' , align : 'left' , width:'100', hidden: true},
|
||||
{ name : 'fromAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
|
||||
{ name : 'toAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
|
||||
{ name : 'author' , align : 'center' , width:'60' },
|
||||
{ name : 'simYn' , align : 'center' , width:'40' },
|
||||
{ name : 'fromAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter, sortable:false },
|
||||
{ name : 'toAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter, sortable:false },
|
||||
{ name : 'author' , align : 'center' , width:'60', sortable:false },
|
||||
{ name : 'simYn' , align : 'center' , width:'40', sortable:false },
|
||||
{ name : 'syncAsyncType' , align : 'center' , width:'40', hidden: true},
|
||||
{ name : 'lastModifiedDate' , align : 'center' , width:'130', sortable:true },
|
||||
],
|
||||
jsonReader : {
|
||||
repeatitems : false
|
||||
},
|
||||
// 서버(ApiInterfaceController#selectList)가 sidx/sord 대신 sortName/sortOrder 파라미터로 정렬조건을 받는다
|
||||
prmNames : { sort : 'sortName', order : 'sortOrder' },
|
||||
sortname : 'lastModifiedDate',
|
||||
sortorder : 'desc',
|
||||
pager : $('#pager'),
|
||||
page : '${param.page}',
|
||||
rowNum : '${rmsDefaultRowNum}',
|
||||
@@ -152,10 +167,6 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for(var i = 0 ; i< colModel.length; i++){
|
||||
$(this).setColProp(colModel[i].name, {sortable : false});
|
||||
}
|
||||
updateSelectionInfo("#grid");
|
||||
},
|
||||
onSelectRow: function(rowid, status, e) {
|
||||
@@ -189,6 +200,9 @@
|
||||
$("select[name=searchEaiBzwkDstcd]").change(function() {
|
||||
search();
|
||||
});
|
||||
$("#chkApiSpec, #chkPortalDisplay").change(function() {
|
||||
search();
|
||||
});
|
||||
$("#btn_search").click(function(){
|
||||
search();
|
||||
});
|
||||
@@ -247,6 +261,18 @@
|
||||
<input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:180px;">포탈 노출</th>
|
||||
<td colspan="3">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="chkApiSpec" <c:if test="${param.searchApiSpecYn eq 'Y'}">checked</c:if>> OpenAPI Spec 등록
|
||||
</label>
|
||||
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="chkPortalDisplay" <c:if test="${param.searchPortalDisplayYn eq 'Y'}">checked</c:if>> 포탈 게시
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table id="grid" ></table>
|
||||
|
||||
@@ -176,13 +176,65 @@
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_app_delete").click(function () {
|
||||
if (!confirm("이 승인 요청을 삭제하시겠습니까?\n삭제 후에는 복구할 수 없습니다.")) {
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: "DELETE", id: key},
|
||||
success: function (args) {
|
||||
alert("삭제되었습니다.");
|
||||
if (window.dialogArguments && window.dialogArguments.self) {
|
||||
window.dialogArguments.self.location.reload();
|
||||
} else if (window.opener) {
|
||||
window.opener.location.reload();
|
||||
}
|
||||
window.close();
|
||||
},
|
||||
error: function (e) {
|
||||
alert(approvalErrorMessage(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn_close").click(function () {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
|
||||
function approvalErrorMessage(e) {
|
||||
try {
|
||||
var json = JSON.parse(e.responseText);
|
||||
return json.errorMsg || e.responseText;
|
||||
} catch (ex) {
|
||||
return e.responseText;
|
||||
}
|
||||
}
|
||||
|
||||
// 요청자(PTL_USER) row 가 삭제되면 상세 조회 자체가 불가능하다. 이 경우 삭제만 가능하도록 노출한다.
|
||||
function showLoadFailure(message) {
|
||||
$("#approvalSubject").text("상세 정보를 조회할 수 없는 승인 요청");
|
||||
$("#approvalDetail").text(message);
|
||||
$("#btn_modify, #btn_app_approve, #btn_app_reject, #btn_app_force_reject, #btn_app_redeploy").hide();
|
||||
$("#gwActionSection, #gwActionResult").hide();
|
||||
document.getElementById("btn_app_delete").style.display = "";
|
||||
}
|
||||
|
||||
var currentRequestType = null; // 현재 신청 유형 (DELETE 승인 시 GW 처리방식 선택 노출용)
|
||||
|
||||
// 값이 있을 때만 행을 노출한다. (해지 신청처럼 해당 항목이 없는 유형에서 빈 행이 남지 않도록)
|
||||
function setOptionalRow(rowId, spanId, value) {
|
||||
if (value) {
|
||||
$("#" + spanId).text(value);
|
||||
$("#" + rowId).show();
|
||||
} else {
|
||||
$("#" + spanId).text("");
|
||||
$("#" + rowId).hide();
|
||||
}
|
||||
}
|
||||
|
||||
function detail(key) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
@@ -264,6 +316,21 @@
|
||||
|
||||
$("#clientName").text(data.clientName);
|
||||
|
||||
// 신청서에 담긴 Client 등록 정보(승인 시 GW 클라이언트로 반영되는 값). 값이 있을 때만 노출
|
||||
setOptionalRow("appDescriptionRow", "appDescription", data.appDescription);
|
||||
setOptionalRow("ipWhitelistRow", "ipWhitelist", data.ipWhitelist);
|
||||
setOptionalRow("callbackUrlRow", "callbackUrl", data.callbackUrl);
|
||||
|
||||
// 신청 사유. 해지 신청은 사유 입력이 필수이므로 "해지 사유"로 표기하고, 그 외 유형은 "신청 사유"
|
||||
if (data.requestReason) {
|
||||
$("#requestReasonLabel").text(data.requestType === 'DELETE' ? "해지 사유" : "신청 사유");
|
||||
$("#requestReason").text(data.requestReason);
|
||||
$("#requestReasonRow").show();
|
||||
} else {
|
||||
$("#requestReason").text("");
|
||||
$("#requestReasonRow").hide();
|
||||
}
|
||||
|
||||
var apiServiceHtml = "";
|
||||
for (var i = 0; i < data.apiServices.length; i++) {
|
||||
var apiService = data.apiServices[i];
|
||||
@@ -318,7 +385,9 @@
|
||||
$("#apisTable tbody").html(apisHtml);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
var message = approvalErrorMessage(e);
|
||||
alert(message);
|
||||
showLoadFailure(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -365,6 +434,22 @@
|
||||
<th style="width:20%;">이름</th>
|
||||
<td><span id="clientName"></span></td>
|
||||
</tr>
|
||||
<tr id="appDescriptionRow" style="display:none;">
|
||||
<th style="width:20%;">앱 설명</th>
|
||||
<td><span id="appDescription" style="white-space: pre-wrap;"></span></td>
|
||||
</tr>
|
||||
<tr id="ipWhitelistRow" style="display:none;">
|
||||
<th style="width:20%;">IP 화이트리스트</th>
|
||||
<td><span id="ipWhitelist" style="white-space: pre-wrap;"></span></td>
|
||||
</tr>
|
||||
<tr id="callbackUrlRow" style="display:none;">
|
||||
<th style="width:20%;">Callback URL</th>
|
||||
<td><span id="callbackUrl" style="white-space: pre-wrap;"></span></td>
|
||||
</tr>
|
||||
<tr id="requestReasonRow" style="display:none;">
|
||||
<th style="width:20%;"><span id="requestReasonLabel">신청 사유</span></th>
|
||||
<td><span id="requestReason" style="white-space: pre-wrap;"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="title">API 목록</div>
|
||||
<table id="apisTable" class="table_row" cellspacing="0">
|
||||
@@ -412,6 +497,9 @@
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_app_force_reject" level="W"><i
|
||||
class="material-icons">block</i>강제 반려
|
||||
</button>
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_app_delete" level="W"><i
|
||||
class="material-icons">delete</i>삭제
|
||||
</button>
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_app_redeploy" level="W"><i
|
||||
class="material-icons">close</i>재처리
|
||||
</button>
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
});
|
||||
|
||||
$("#btn_user_delete").click(function() {
|
||||
if (!confirm("삭제된 사용자 또는 법인이 포함되어 승인·반려할 수 없는 요청입니다.\n이 승인 요청을 삭제하시겠습니까?")) {
|
||||
if (!confirm("삭제된 요청자·사용자 또는 법인이 포함되어 승인·반려할 수 없는 요청입니다.\n이 승인 요청을 삭제하시겠습니까?\n삭제 후에는 복구할 수 없습니다.")) {
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
@@ -198,7 +198,7 @@
|
||||
window.close();
|
||||
},
|
||||
error: function(e) {
|
||||
alert(e.responseText);
|
||||
alert(approvalErrorMessage(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -362,10 +362,31 @@
|
||||
}
|
||||
},
|
||||
error: function(e) {
|
||||
alert(e.responseText);
|
||||
var message = approvalErrorMessage(e);
|
||||
alert(message);
|
||||
showLoadFailure(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function approvalErrorMessage(e) {
|
||||
try {
|
||||
var json = JSON.parse(e.responseText);
|
||||
return json.errorMsg || e.responseText;
|
||||
} catch (ex) {
|
||||
return e.responseText;
|
||||
}
|
||||
}
|
||||
|
||||
// 요청자(PTL_USER) row 가 삭제되면 상세 조회 자체가 불가능하다. 이 경우 삭제만 가능하도록 노출한다.
|
||||
function showLoadFailure(message) {
|
||||
$("#approvalSubject").text("상세 정보를 조회할 수 없는 승인 요청");
|
||||
$("#approvalDetail").text(message);
|
||||
$("#approvalDetailRow").show();
|
||||
$("#btn_unmask").hide();
|
||||
$("#btn_user_approve, #btn_user_reject, #btn_user_force_reject, #btn_user_redeploy").hide();
|
||||
document.getElementById("btn_user_delete").style.display = "";
|
||||
}
|
||||
function updateDetailView(data) {
|
||||
$("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용");
|
||||
$("#approvalStatus").text(data.approvalStatus.description);
|
||||
|
||||
@@ -62,7 +62,19 @@
|
||||
return '<span title="' + escapeAttr(rowObject.sentDateFull || '') + '">' + escapeHtmlJs(rowObject.sentDateShort || '') + '</span>';
|
||||
}
|
||||
|
||||
function toDashDate(yyyymmdd) {
|
||||
return yyyymmdd.substring(0, 4) + "-" + yyyymmdd.substring(4, 6) + "-" + yyyymmdd.substring(6, 8);
|
||||
}
|
||||
|
||||
function init() {
|
||||
$("#startDatepicker").datepicker().inputmask("yyyy-mm-dd",{'autoUnmask':true});
|
||||
$("#endDatepicker").datepicker().inputmask("yyyy-mm-dd",{'autoUnmask':true});
|
||||
|
||||
var today = getToday();
|
||||
|
||||
$("input[name=searchFromDate]").val(toDashDate(today.substring(0,6)+"01"));
|
||||
$("input[name=searchToDate]").val(toDashDate(today));
|
||||
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'LIST_INIT_COMBO'},
|
||||
@@ -145,6 +157,20 @@
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var start = $("input[name=searchFromDate]").val().replace(/-/gi, "");
|
||||
var end = $("input[name=searchToDate]").val().replace(/-/gi, "");
|
||||
|
||||
if (start && start > getToday()) {
|
||||
alert("시작일이 오늘 이후입니다. 시작일을 확인해주세요.");
|
||||
$("input[name=searchFromDate]").focus();
|
||||
return false;
|
||||
}
|
||||
if (start && end && start > end) {
|
||||
alert("조회기간 종료일은 시작일보다 커야합니다.");
|
||||
$("input[name=searchToDate]").focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
@@ -198,7 +224,9 @@
|
||||
</td>
|
||||
<th>요청기간</th>
|
||||
<td colspan="3">
|
||||
<input type="date" name="searchFromDate"> ~ <input type="date" name="searchToDate">
|
||||
<input type="text" name="searchFromDate" id="startDatepicker" readonly="readonly" value="" size="10" style="width:100px; border:1px solid #ebebec;">
|
||||
~
|
||||
<input type="text" name="searchToDate" value="" id="endDatepicker" size="10" readonly="readonly" style="width:100px; border:1px solid #ebebec;">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
$('#sentDate').text(data.sentDateFull || '');
|
||||
// 서버에서 마스킹+escape 된 안전 HTML
|
||||
$('#messageHtml').html(data.messageHtml || '');
|
||||
// 응답내용(RESPONSE_DATA) — 원문이므로 text 로만 출력
|
||||
$('#responseData').text(data.responseData || '');
|
||||
|
||||
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
|
||||
if (data.requestStatus !== 'PENDING') {
|
||||
@@ -142,6 +144,10 @@
|
||||
<th>메세지 내용</th>
|
||||
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>응답내용</th>
|
||||
<td colspan="3"><div id="responseData" class="msg-detail"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
<td>
|
||||
<input type="text" id="keywordList" style="width:100%" placeholder="쉼표(,)로 구분하여 입력">
|
||||
<div class="help-text" style="color:#666; font-size:12px; margin-top:5px;">
|
||||
예시: #인증, #대출, #예금
|
||||
예시: 인증, 대출, 예금
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -327,14 +327,61 @@
|
||||
}, 'json').fail(ajaxFail);
|
||||
}
|
||||
|
||||
// ── 캐시 Reload ──
|
||||
var reloadModal;
|
||||
var reloadBtnHtml;
|
||||
|
||||
function reloadPortalCache() {
|
||||
var $btn = $('#btnReloadCache');
|
||||
if ($btn.prop('disabled')) return;
|
||||
if (reloadBtnHtml === undefined) reloadBtnHtml = $btn.html();
|
||||
$btn.prop('disabled', true)
|
||||
.html('<span class="spinner-border spinner-border-sm"></span> 리로드 중...');
|
||||
|
||||
$.post(url, { cmd: 'TRANSACTION_RELOAD' }, function (result) {
|
||||
if (result && result.success) {
|
||||
alert('포탈 메뉴 캐시 리로드 완료 (항목 ' + result.itemCount + '건, ' + result.reloadedAt + ')');
|
||||
} else {
|
||||
alert('캐시 리로드 실패: ' + (result ? result.message : '알 수 없는 오류'));
|
||||
}
|
||||
}, 'json').fail(ajaxFail);
|
||||
showReloadResult(result);
|
||||
}, 'json').fail(ajaxFail).always(function () {
|
||||
$btn.prop('disabled', false).html(reloadBtnHtml);
|
||||
});
|
||||
}
|
||||
|
||||
function showReloadResult(result) {
|
||||
if (!result) {
|
||||
alert('캐시 리로드 실패: 알 수 없는 오류');
|
||||
return;
|
||||
}
|
||||
|
||||
var servers = result.servers || [];
|
||||
var $summary = $('#reloadSummary');
|
||||
$summary.removeClass('alert-success alert-danger alert-warning')
|
||||
.addClass(result.success ? 'alert-success'
|
||||
: (result.successCount > 0 ? 'alert-warning' : 'alert-danger'))
|
||||
.text(result.message || '');
|
||||
|
||||
var rows = '';
|
||||
servers.forEach(function (s) {
|
||||
var badge = s.success
|
||||
? '<span class="badge text-bg-success">성공</span>'
|
||||
: '<span class="badge text-bg-danger">실패</span>';
|
||||
var detail = s.success
|
||||
? ('항목 ' + (s.itemCount == null ? '-' : s.itemCount) + '건'
|
||||
+ (s.reloadedAt ? ' / ' + escapeHtml(String(s.reloadedAt)) : ''))
|
||||
: escapeHtml(String(s.message || '-'));
|
||||
rows += '<tr>'
|
||||
+ '<td class="text-center">' + badge + '</td>'
|
||||
+ '<td style="word-break:break-all;">' + escapeHtml(String(s.url || '')) + '</td>'
|
||||
+ '<td>' + detail + '</td>'
|
||||
+ '<td class="text-end">' + (s.elapsedMs == null ? '-' : s.elapsedMs + 'ms') + '</td>'
|
||||
+ '</tr>';
|
||||
});
|
||||
if (rows === '') {
|
||||
rows = '<tr><td colspan="4" class="text-center text-muted">'
|
||||
+ '호출 대상 서버가 없습니다. 아래 설정 안내를 확인하세요.</td></tr>';
|
||||
}
|
||||
$('#reloadResultBody').html(rows);
|
||||
|
||||
if (!reloadModal) reloadModal = new bootstrap.Modal(document.getElementById('reloadResultModal'));
|
||||
reloadModal.show();
|
||||
}
|
||||
|
||||
function ajaxFail(xhr) {
|
||||
@@ -453,7 +500,7 @@
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" level="W" onclick="initializeMenus()">
|
||||
<i class="bi bi-arrow-counterclockwise"></i> 메뉴 초기화</button>
|
||||
<span class="spacer"></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" level="W" onclick="reloadPortalCache()">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnReloadCache" level="W" onclick="reloadPortalCache()">
|
||||
<i class="bi bi-arrow-repeat"></i> 캐시 Reload</button>
|
||||
<button type="button" class="btn btn-sm btn-success" level="W" onclick="savePlacements()">
|
||||
<i class="bi bi-save"></i> 저장</button>
|
||||
@@ -463,6 +510,12 @@
|
||||
드래그로 위치를 조절한 뒤 <strong>저장</strong> 버튼으로 일괄 반영합니다.
|
||||
기본 항목([기본] 배지)은 [-] 시 <strong>미배치</strong>로만 이동하고, 커스텀 항목은 미배치 시 <strong>삭제</strong>됩니다.
|
||||
저장 후 <strong>캐시 Reload</strong> 를 실행해야 포탈에 즉시 반영됩니다(미실행 시 TTL 경과 후 반영).
|
||||
<br>
|
||||
캐시 Reload 대상 서버는 포탈 프로퍼티 <code>Portal / portal.internal.menu-reload-url</code> 로 설정합니다.
|
||||
포탈이 <strong>다중 서버(이중화)</strong> 로 기동된 경우 모든 서버의 리로드 URL 을
|
||||
<strong>콤마(,) 또는 줄바꿈</strong> 으로 나열해야 전체 서버에 반영됩니다
|
||||
(예: <code>http://10.0.0.11:39130/internal/menu/reload, http://10.0.0.12:39130/internal/menu/reload</code>).
|
||||
서버별 요청 타임아웃은 <strong>3초</strong> 이며, 실행 결과는 서버별로 표시됩니다.
|
||||
</div>
|
||||
|
||||
<div class="menu-board">
|
||||
@@ -544,6 +597,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 캐시 Reload 결과 -->
|
||||
<div class="modal fade" id="reloadResultModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">포탈 메뉴 캐시 Reload 결과</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert py-2 px-3" id="reloadSummary" style="font-size:13px;"></div>
|
||||
<table class="table table-sm table-bordered align-middle" style="font-size:12px;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:70px;" class="text-center">결과</th>
|
||||
<th>서버 URL</th>
|
||||
<th style="width:38%;">상세</th>
|
||||
<th style="width:80px;" class="text-end">소요</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reloadResultBody"></tbody>
|
||||
</table>
|
||||
<div class="text-muted" style="font-size:12px;">
|
||||
대상 서버는 포탈 프로퍼티 <code>Portal / portal.internal.menu-reload-url</code> 에
|
||||
콤마(,) 또는 줄바꿈으로 나열합니다. 서버별 타임아웃 3초, 전 서버 병렬 호출입니다.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
|
||||
@@ -118,6 +118,9 @@
|
||||
$('.incident-endat-hint').hide();
|
||||
$('#endAt').prop('readonly', false);
|
||||
}
|
||||
// 양식(템플릿)은 장애 공지에만 쓴다 - 점검은 문구 체계가 다르다
|
||||
$('.template-row').toggle(t === NOTICE_TYPE_INCIDENT);
|
||||
|
||||
// 타임라인은 등록된 장애(수정 모드)에서만 다룬다
|
||||
if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) {
|
||||
$('#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) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.innerHTML = text;
|
||||
@@ -189,8 +217,11 @@
|
||||
// AuthorizeInterceptor 가 정상적으로 권한 인식. window.open 직접 호출 시 권한 부족 발생.
|
||||
// 본 페이지는 summernote 본문 에디터 등 다른 iframe 이 함께 존재할 수 있어
|
||||
// frames[0] 가정은 위험 → DOM 에서 .ui-dialog 내 iframe 중 src=apiGroupMan 인 것을 식별.
|
||||
// 공지 영향 API 는 포탈에 실제 노출되는 것만 고르면 되므로 두 조건을 켠 채로 연다.
|
||||
// (타 화면에서 여는 팝업은 파라미터가 없어 해제 상태 — 팝업 기본값은 건드리지 않는다)
|
||||
var selectedIds = affectedApis.map(function(a) { return a.apiId; }).join(',');
|
||||
var url2 = apiPopupUrl + '?cmd=POPUP&selectedApiIds=' + encodeURIComponent(selectedIds);
|
||||
var url2 = apiPopupUrl + '?cmd=POPUP&searchApiSpecYn=Y&searchPortalDisplayYn=Y'
|
||||
+ '&selectedApiIds=' + encodeURIComponent(selectedIds);
|
||||
var args = {};
|
||||
showModal(url2, args, 1280, 860, function () {
|
||||
var doc = args.self && args.self.document;
|
||||
@@ -438,6 +469,15 @@
|
||||
|
||||
$('#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_removeAffectedApi').click(removeSelectedAffectedApis);
|
||||
|
||||
@@ -490,6 +530,10 @@
|
||||
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) return;
|
||||
|
||||
var formData = new FormData($("#ajaxForm")[0]);
|
||||
// Summernote 그림 다이얼로그의 내부 파일 input(name="files")이 #ajaxForm 안에 있어
|
||||
// 선택한 이미지가 지워지지 않고 남아 있으면 여기서 함께 딸려 온다.
|
||||
// 본 화면은 첨부파일 기능을 쓰지 않으므로 항상 제거한다.
|
||||
formData.delete("files");
|
||||
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
|
||||
formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N");
|
||||
formData.set("noticeDetail", $('#contents').summernote('code'));
|
||||
@@ -587,7 +631,12 @@
|
||||
</td>
|
||||
</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">
|
||||
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
|
||||
</td>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
if ($('input[name=prptyName]').val() == "") {
|
||||
alert("<%= localeMessage.getString("propertyDetail.checkRequired2") %>");
|
||||
return false;
|
||||
} else if ($('input[name=prpty2Val]').val() == "") {
|
||||
} else if ($('[name=prpty2Val]').val() == "") {
|
||||
alert("<%= localeMessage.getString("propertyDetail.checkRequired3") %>");
|
||||
return false;
|
||||
}
|
||||
@@ -90,7 +90,19 @@
|
||||
'설명',
|
||||
'<%= localeMessage.getString("propertyDetail.delYn") %>'],
|
||||
colModel: [{name: 'PRPTYNAME', width: 50, align: 'left', editable: true},
|
||||
{name: 'PRPTY2VAL', width: 150, align: 'left', editable: true},
|
||||
{
|
||||
name: 'PRPTY2VAL',
|
||||
width: 150,
|
||||
align: 'left',
|
||||
editable: true,
|
||||
//다행(멀티라인) 값 편집 — 관련 사이트 목록처럼 줄 단위 표기하는 값 지원
|
||||
edittype: 'textarea',
|
||||
editoptions: {rows: 3, cols: 60},
|
||||
//줄바꿈이 그리드에서도 그대로 보이도록
|
||||
cellattr: function () {
|
||||
return ' style="white-space:pre-line; word-break:break-all;"';
|
||||
}
|
||||
},
|
||||
{name: 'PRPTYDESC', width: 200, align: 'left', editable: true, edittype: 'textarea', editoptions: {rows: 2}},
|
||||
{
|
||||
name: 'DELETEYN',
|
||||
@@ -265,7 +277,7 @@
|
||||
}
|
||||
var data = new Object();
|
||||
data["PRPTYNAME"] = $("input[name=prptyName]").val();
|
||||
data["PRPTY2VAL"] = $("input[name=prpty2Val]").val();
|
||||
data["PRPTY2VAL"] = $("[name=prpty2Val]").val();
|
||||
data["PRPTYDESC"] = $("input[name=prptyDesc]").val();
|
||||
|
||||
var rows = $("#grid")[0].rows;
|
||||
@@ -346,7 +358,8 @@
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("propertyDetail.propertyValue") %>
|
||||
</th>
|
||||
<td colspan="3"><input type="text" name="prpty2Val"/>
|
||||
<td colspan="3"><textarea name="prpty2Val" rows="3"
|
||||
style="width:100%; box-sizing:border-box; resize:vertical;"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.url-cell { display:inline-block; max-width:380px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:middle; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/webhook/webhookReqMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/webhook/webhookReqMan.view" />';
|
||||
|
||||
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
|
||||
function escapeAttr(s) { return escapeHtmlJs(s).replace(/"/g, '"'); }
|
||||
|
||||
function formatTargetUrl(cellvalue, options, rowObject) {
|
||||
var v = cellvalue || '';
|
||||
return '<span class="url-cell" title="' + escapeAttr(v) + '">' + escapeHtmlJs(v) + '</span>';
|
||||
}
|
||||
|
||||
function init() {
|
||||
|
||||
// 뒤로가기 등으로 이전 검색조건 파라미터가 있으면 기본값을 덮어씀
|
||||
putSelectFromParam();
|
||||
|
||||
buildGrid();
|
||||
}
|
||||
|
||||
function buildGrid() {
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['id', 'No', '기관명', '리버스 프록시 경로', 'Target URL', '신청일시'],
|
||||
colModel: [
|
||||
{name: 'id', align: 'center', key: true, hidden: true},
|
||||
{name: 'rowNum', align: 'center', width: 45, sortable: false},
|
||||
{name: 'orgName', align: 'left', width: 160},
|
||||
{name: 'reverseProxyPath', align: 'left', width: 160},
|
||||
{name: 'targetUrl', align: 'left', width: 380, formatter: formatTargetUrl},
|
||||
{name: 'createdDate', align: 'center', width: 140}
|
||||
],
|
||||
jsonReader: {repeatitems: false},
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: $("#container").height(),
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList: eval('[${rmsDefaultRowList}]'),
|
||||
ondblClickRow: function (rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var id = rowData['id'];
|
||||
var url2 = url_view + '?cmd=DETAIL';
|
||||
url2 += '&page=' + $(this).getGridParam("page");
|
||||
url2 += '&returnUrl=' + getReturnUrl();
|
||||
url2 += '&menuId=' + '${param.menuId}';
|
||||
url2 += '&id=' + id;
|
||||
url2 += '&' + getSearchUrl();
|
||||
goNav(url2);
|
||||
},
|
||||
loadComplete: function () {
|
||||
var page = $(this).getGridParam('page');
|
||||
var rowNum = $(this).getGridParam('rowNum');
|
||||
var rows = $(this).getDataIDs();
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var number = ((page - 1) * rowNum + i) + 1;
|
||||
$(this).setCell(rows[i], 'rowNum', number);
|
||||
}
|
||||
// 서버 고정 정렬(신청일시 역순) — 컬럼 정렬 비활성
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, {sortable: false});
|
||||
}
|
||||
},
|
||||
loadError: function (jqXHR, textStatus, errorThrown) {
|
||||
var location = '<%=request.getContextPath()%>/';
|
||||
comloadError(jqXHR, textStatus, errorThrown, location);
|
||||
}
|
||||
});
|
||||
resizeJqGridWidth('grid', 'content_middle', '1000');
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("select[name^=search]").change(function () { $("#btn_search").click(); });
|
||||
$("input[name^=search]").keydown(function (key) {
|
||||
if (key.keyCode == 13) { $("#btn_search").click(); }
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
</div>
|
||||
<div class="title">웹훅 신청내역<span class="tooltip">등록된 웹훅 신청 내역을 조회합니다.</span></div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<colgroup>
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>기관명</th>
|
||||
<td><input type="text" name="searchOrgName" autocomplete="off"></td>
|
||||
<th>리버스 프록시 경로</th>
|
||||
<td><input type="text" name="searchReverseProxyPath" autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Target URL</th>
|
||||
<td colspan="3"><input type="text" name="searchTargetUrl" autocomplete="off" style="width:95%;"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,154 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.sub_list { width:100%; border-collapse:collapse; }
|
||||
.sub_list th, .sub_list td { border:1px solid #ebebec; padding:6px 8px; text-align:left; }
|
||||
.sub_list th { background:#f7f7f8; width:60px; }
|
||||
.empty-msg { color:#999; padding:6px 2px; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/webhook/webhookReqMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/webhook/webhookReqMan.view" />';
|
||||
var key = '${param.id}';
|
||||
var combo;
|
||||
|
||||
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
|
||||
|
||||
// 이벤트유형 코드 → 모니터링 공통코드(EVENT_TYPE) 코드명
|
||||
function eventTypeName(code) {
|
||||
if (!combo || !combo.eventTypeList) { return code; }
|
||||
for (var i = 0; i < combo.eventTypeList.length; i++) {
|
||||
if (combo.eventTypeList[i].CODE == code) { return combo.eventTypeList[i].NAME; }
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function renderApiList(apiIds) {
|
||||
var $tbody = $('#apiListBody').empty();
|
||||
if (!apiIds || apiIds.length === 0) {
|
||||
$tbody.append('<tr><td class="empty-msg">등록된 API가 없습니다.</td></tr>');
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < apiIds.length; i++) {
|
||||
$tbody.append('<tr><td>' + escapeHtmlJs(apiIds[i]) + '</td></tr>');
|
||||
}
|
||||
}
|
||||
|
||||
function renderEventTypes(eventTypes) {
|
||||
if (!eventTypes || eventTypes.length === 0) {
|
||||
$('#eventTypes').text('');
|
||||
return;
|
||||
}
|
||||
var names = eventTypes.map(function (code) { return eventTypeName(code); });
|
||||
$('#eventTypes').text(names.join(', '));
|
||||
}
|
||||
|
||||
function detail() {
|
||||
if (!key) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'DETAIL', id: key},
|
||||
success: function (data) {
|
||||
$('#id').text(data.id != null ? data.id : '');
|
||||
$('#orgName').text(data.orgName || '');
|
||||
$('#reverseProxyPath').text(data.reverseProxyPath || '');
|
||||
$('#targetUrl').text(data.targetUrl || '');
|
||||
$('#secret').text(data.secret || '');
|
||||
$('#userSecret').text(data.userSecret || '');
|
||||
$('#createdDate').text(data.createdDate || '');
|
||||
|
||||
renderApiList(data.apiIds);
|
||||
renderEventTypes(data.eventTypes);
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
|
||||
buttonControl();
|
||||
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'LIST_INIT_COMBO'},
|
||||
success: function (json) {
|
||||
combo = json;
|
||||
detail();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
|
||||
$('#btn_previous').click(function () { goNav(returnUrl); });
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div>
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title" id="title">웹훅 신청내역 상세</div>
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>신청 ID</th>
|
||||
<td colspan="3"><span id="id"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>기관명</th>
|
||||
<td><span id="orgName"></span></td>
|
||||
<th>신청일시</th>
|
||||
<td><span id="createdDate"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>리버스 프록시 경로</th>
|
||||
<td><span id="reverseProxyPath"></span></td>
|
||||
<th>Target URL</th>
|
||||
<td><span id="targetUrl"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Secret</th>
|
||||
<td colspan="3"><span id="secret"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>User Secret</th>
|
||||
<td colspan="3"><span id="userSecret"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>신청 이벤트</th>
|
||||
<td colspan="3"><span id="eventTypes"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="title" style="margin-top:20px;">API 목록</div>
|
||||
<table class="table_row">
|
||||
<colgroup><col style="width:100%"/></colgroup>
|
||||
<thead><tr><th style="width:100%;">API ID</th></tr></thead>
|
||||
<tbody id="apiListBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -89,7 +89,7 @@
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '발송서버', '발송일시'],
|
||||
colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '재시도', '발송서버', '발송일시'],
|
||||
colModel: [
|
||||
{name: 'id', align: 'center', key: true, hidden: true},
|
||||
{name: 'rowNum', align: 'center', width: 45, sortable: false},
|
||||
@@ -99,11 +99,12 @@
|
||||
{name: 'proxyUrl', align: 'left', width: 300, formatter: formatTargetUrl},
|
||||
{name: 'statusCode', align: 'center', width: 60},
|
||||
{name: 'success', align: 'center', width: 70, formatter: formatSuccess},
|
||||
{name: 'retryCount', align: 'center', width: 60},
|
||||
{name: 'sendBy', align: 'center', width: 100},
|
||||
{name: 'sentAt', align: 'center', width: 140}
|
||||
],
|
||||
jsonReader: {repeatitems: false},
|
||||
pager: $('#pager'),
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
@@ -222,9 +223,9 @@
|
||||
<td><input type="text" name="searchTargetUrl" autocomplete="off" style="width:95%;"></td>
|
||||
<th>발송기간</th>
|
||||
<td>
|
||||
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:40%; border:1px solid #ebebec;">
|
||||
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:100px; border:1px solid #ebebec;">
|
||||
~
|
||||
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:40%; border:1px solid #ebebec;">
|
||||
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:100px; border:1px solid #ebebec;">
|
||||
<input type="hidden" name="searchStartDate" value="">
|
||||
<input type="hidden" name="searchEndDate" value="">
|
||||
</td>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
$('#sendBy').text(data.sendBy || '');
|
||||
$('#sentAt').text(data.sentAt || '');
|
||||
$('#signature').text(data.signature || '');
|
||||
$('#userSecret').text(data.userSecret || '');
|
||||
$('#errorMessage').text(data.errorMessage || '');
|
||||
$('#payload').text(data.payload || '');
|
||||
$('#responseBody').text(data.responseBody || '');
|
||||
@@ -116,7 +117,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Signature</th>
|
||||
<td colspan="3"><span id="signature"></span></td>
|
||||
<td><span id="signature"></span></td>
|
||||
<th>User Secret</th>
|
||||
<td><span id="userSecret"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payload</th>
|
||||
|
||||
@@ -33,6 +33,18 @@
|
||||
width: 100%;
|
||||
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>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
||||
@@ -58,6 +70,7 @@
|
||||
|
||||
// 총건수 도넛 차트
|
||||
var totalDonutOption = {
|
||||
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||
title: {
|
||||
text: '총 건수 분포',
|
||||
left: 'center',
|
||||
@@ -99,7 +112,7 @@
|
||||
show: true
|
||||
},
|
||||
data: [
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||
]
|
||||
@@ -286,6 +299,28 @@
|
||||
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) {
|
||||
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
|
||||
console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
|
||||
@@ -416,12 +451,13 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'API ID', 'API명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
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: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
@@ -458,7 +494,7 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||
'Inbound Adapter', 'Outbound Adapter',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
@@ -505,6 +541,8 @@
|
||||
$("input[name=searchStartDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||
$("input[name=searchEndDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||
|
||||
$("input[name=searchStartDateTime], input[name=searchEndDateTime]").datepicker();
|
||||
|
||||
// 기본값 설정 (어제 기준 해당 월 1일 ~ 어제)
|
||||
var today = getToday();
|
||||
var todayStr = today.replace(/-/g, '');
|
||||
@@ -544,6 +582,14 @@
|
||||
search();
|
||||
});
|
||||
|
||||
$("#btnPrevMonth").click(function() {
|
||||
shiftMonthRange(-1);
|
||||
});
|
||||
|
||||
$("#btnNextMonth").click(function() {
|
||||
shiftMonthRange(1);
|
||||
});
|
||||
|
||||
$("#btn_excel_export_summary").click(function() {
|
||||
exportToExcel(true);
|
||||
});
|
||||
@@ -598,11 +644,13 @@
|
||||
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" 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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API명</th>
|
||||
<th>API ID</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
@@ -639,11 +687,11 @@
|
||||
</div>
|
||||
<div id="callChart" class="chart"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 요약 그리드 -->
|
||||
<div style="margin-top: 20px;">
|
||||
<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">
|
||||
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
||||
</button>
|
||||
|
||||
@@ -33,6 +33,18 @@
|
||||
width: 100%;
|
||||
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>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<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_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.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;
|
||||
|
||||
@@ -59,6 +73,7 @@
|
||||
|
||||
// 총건수 도넛 차트
|
||||
var totalDonutOption = {
|
||||
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||
title: {
|
||||
text: '총 건수 분포',
|
||||
left: 'center',
|
||||
@@ -100,7 +115,7 @@
|
||||
show: true
|
||||
},
|
||||
data: [
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||
]
|
||||
@@ -149,23 +164,20 @@
|
||||
var hour = parseInt(statTime.substring(8, 10));
|
||||
|
||||
var startDate = year + '-' + month + '-' + day;
|
||||
var startTime = (hour < 10 ? '0' + hour : hour) + ':00';
|
||||
|
||||
// 1시간 후 계산 (23시인 경우 다음날 00시로 자동 계산)
|
||||
var d = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), hour, 0, 0);
|
||||
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 hourStr = (hour < 10 ? '0' + hour : hour);
|
||||
var startTime = hourStr + ':00';
|
||||
// 해당 시간대 끝(정시~59분)으로 이동 (예: 10:00~10:59)
|
||||
var endTime = hourStr + ':59';
|
||||
|
||||
var params = '?searchStartDate=' + startDate + '&searchStartTime=' + startTime
|
||||
+ '&searchEndDate=' + endDate + '&searchEndTime=' + endTime;
|
||||
params += '&menuId='+'${param.menuId}';
|
||||
+ '&searchEndDate=' + startDate + '&searchEndTime=' + endTime;
|
||||
params += '&menuId='+DASHBOARD_MENU_ID;
|
||||
params += '&cmd='+'LIST';
|
||||
params += '&serviceType='+'APIGW';
|
||||
|
||||
// 왼쪽 메뉴도 대시보드(분단위 통계) 메뉴로 전환
|
||||
parent.leftFrame.location.href = url_left_menu + '?menuId=' + DASHBOARD_MENU_ID
|
||||
+ '&serviceType=' + sessionStorage["serviceType"];
|
||||
location.href = url_minute_view + params;
|
||||
}
|
||||
|
||||
@@ -292,6 +304,25 @@
|
||||
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) {
|
||||
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
|
||||
console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
|
||||
@@ -418,12 +449,13 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'API ID', 'API 명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
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: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
@@ -459,8 +491,8 @@
|
||||
datatype: "json",
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||
colNames: [
|
||||
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||
'Inbound Adapter', 'Outbound Adapter',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
@@ -506,6 +538,8 @@
|
||||
// 날짜 입력 마스크
|
||||
$("input[name=searchDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||
|
||||
$("input[name=searchDate]").datepicker();
|
||||
|
||||
// 기본값 설정 (오늘)
|
||||
var today = getToday();
|
||||
if (!$("input[name=searchDate]").val()) {
|
||||
@@ -524,6 +558,14 @@
|
||||
search();
|
||||
});
|
||||
|
||||
$("#btnPrevDate").click(function() {
|
||||
shiftSearchDate(-1);
|
||||
});
|
||||
|
||||
$("#btnNextDate").click(function() {
|
||||
shiftSearchDate(1);
|
||||
});
|
||||
|
||||
$("#btn_excel_export_summary").click(function() {
|
||||
exportToExcel(true);
|
||||
});
|
||||
@@ -576,11 +618,13 @@
|
||||
<th>조회일자</th>
|
||||
<td colspan="5">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API명</th>
|
||||
<th>API ID</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
@@ -621,7 +665,7 @@
|
||||
<!-- 요약 그리드 -->
|
||||
<div style="margin-top: 20px;">
|
||||
<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">
|
||||
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
||||
</button>
|
||||
|
||||
@@ -26,6 +26,18 @@
|
||||
border: 1px solid #ddd;
|
||||
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>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
||||
@@ -51,6 +63,9 @@
|
||||
|
||||
// 총건수 도넛 차트
|
||||
var totalDonutOption = {
|
||||
// updateCharts()에서 series[].data를 itemStyle 없이 재설정하면 조각별 itemStyle.color가
|
||||
// 병합 과정에서 유실되므로, 옵션 레벨 color 팔레트(성공/Timeout/시스템오류 순)로 색을 고정한다.
|
||||
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||
title: {
|
||||
text: '총 건수 분포',
|
||||
left: 'center',
|
||||
@@ -92,7 +107,7 @@
|
||||
show: true
|
||||
},
|
||||
data: [
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||
]
|
||||
@@ -203,7 +218,6 @@
|
||||
function validateAndAdjustDateRange() {
|
||||
var startDate = $("input[name=searchStartDate]").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, "");
|
||||
|
||||
if (!startDate || !startTime) return null;
|
||||
@@ -219,35 +233,43 @@
|
||||
);
|
||||
}
|
||||
|
||||
// UI 업데이트 헬퍼
|
||||
function updateEndDateTime(dt) {
|
||||
var d = dt.getFullYear() +
|
||||
// 종료일시(yyyyMMddHHmm) 포맷 헬퍼
|
||||
function formatDateTime(dt) {
|
||||
return dt.getFullYear() +
|
||||
String(dt.getMonth() + 1).padStart(2, '0') +
|
||||
String(dt.getDate()).padStart(2, '0');
|
||||
var t = String(dt.getHours()).padStart(2, '0') +
|
||||
String(dt.getDate()).padStart(2, '0') +
|
||||
String(dt.getHours()).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 startDateTime = startDate + startTime;
|
||||
var endDateTime;
|
||||
|
||||
if (endDate && endTime) {
|
||||
var end = parseDateTime(endDate, endTime);
|
||||
if (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);
|
||||
|
||||
if (diffMinutes > 60) {
|
||||
// 1시간 초과 시 시작 + 1시간으로 조정
|
||||
endDateTime = updateEndDateTime(new Date(start.getTime() + 60 * 60 * 1000));
|
||||
// 1시간 초과 시 시작시각 기준 1시간치(정시~59분)로 조정
|
||||
endDateTime = updateEndTime(new Date(start.getTime() + 59 * 60 * 1000));
|
||||
} else {
|
||||
endDateTime = endDate + endTime;
|
||||
endDateTime = formatDateTime(end);
|
||||
}
|
||||
} else {
|
||||
// 종료시간 미입력 시 시작 + 1시간으로 설정
|
||||
endDateTime = updateEndDateTime(new Date(start.getTime() + 60 * 60 * 1000));
|
||||
// 종료시간 미입력 시 시작시각 기준 1시간치(정시~59분)로 설정
|
||||
endDateTime = updateEndTime(new Date(start.getTime() + 59 * 60 * 1000));
|
||||
}
|
||||
|
||||
return { start: startDateTime, end: endDateTime };
|
||||
@@ -302,6 +324,45 @@
|
||||
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) {
|
||||
var cmdType = isSummary ? 'EXCEL_EXPORT_SUMMARY' : 'EXCEL_EXPORT';
|
||||
console.log('[Excel Export] Starting... (type: ' + cmdType + ')');
|
||||
@@ -317,13 +378,10 @@
|
||||
serviceType: '${param.serviceType}'
|
||||
};
|
||||
|
||||
var startDate = $("input[name=searchStartDate]").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, "");
|
||||
if (startDate && startTime) {
|
||||
postData.searchStartDateTime = startDate + startTime;
|
||||
postData.searchEndDateTime = endDate + endTime;
|
||||
var range = validateAndAdjustDateRange();
|
||||
if (range) {
|
||||
postData.searchStartDateTime = range.start;
|
||||
postData.searchEndDateTime = range.end;
|
||||
}
|
||||
|
||||
console.log('[Excel Export] Request data:', postData);
|
||||
@@ -431,12 +489,13 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'API ID', 'API 명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
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: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
@@ -473,7 +532,7 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||
'Inbound Adapter', 'Outbound Adapter',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
@@ -517,12 +576,14 @@
|
||||
|
||||
$(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 });
|
||||
|
||||
// 기본값 설정 (현재 시간(분) 기준 1시간 전 ~ 현재 시간(분))
|
||||
$("input[name=searchStartDate]").datepicker();
|
||||
|
||||
// 기본값 설정 (현재 시간(분) 기준 59분 전 ~ 현재 시간(분))
|
||||
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) {
|
||||
return d.getFullYear() + '-' +
|
||||
@@ -537,8 +598,7 @@
|
||||
$("input[name=searchStartDate]").val(formatDate(oneHourAgo));
|
||||
$("input[name=searchStartTime]").val(formatTime(oneHourAgo));
|
||||
}
|
||||
if (!$("input[name=searchEndDate]").val()) {
|
||||
$("input[name=searchEndDate]").val(formatDate(now));
|
||||
if (!$("input[name=searchEndTime]").val()) {
|
||||
$("input[name=searchEndTime]").val(formatTime(now));
|
||||
}
|
||||
|
||||
@@ -554,6 +614,14 @@
|
||||
search();
|
||||
});
|
||||
|
||||
$("#btnPrevTime").click(function() {
|
||||
shiftTimeRange(-1);
|
||||
});
|
||||
|
||||
$("#btnNextTime").click(function() {
|
||||
shiftTimeRange(1);
|
||||
});
|
||||
|
||||
$("#btn_excel_export_summary").click(function() {
|
||||
exportToExcel(true);
|
||||
});
|
||||
@@ -568,8 +636,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 종료일/시간 변경 시 1시간 초과 검증
|
||||
$("input[name=searchEndDate], input[name=searchEndTime]").on('change blur', function() {
|
||||
// 종료시각 변경 시 1시간 초과 검증
|
||||
$("input[name=searchEndTime]").on('change blur', function() {
|
||||
validateAndAdjustDateRange();
|
||||
});
|
||||
|
||||
@@ -601,16 +669,21 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:100px;">조회기간</th>
|
||||
<td colspan="3">
|
||||
<input type="text" name="searchStartDate" value="${param.searchStartDate}" style="width:100px;">
|
||||
<input type="text" name="searchStartTime" value="${param.searchStartTime}" style="width:60px;">
|
||||
<td style="width:400px">
|
||||
<input type="text" name="searchStartDate" value="${param.searchStartDate}" style="width:80px;">
|
||||
<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:60px;">
|
||||
<input type="text" name="searchEndTime" value="${param.searchEndTime}" style="width:40px;">
|
||||
<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>
|
||||
</td>
|
||||
<th style="width:100px;">API ID</th>
|
||||
<td style="width:400px">
|
||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- 도넛 차트 (상단 50%씩) -->
|
||||
<div class="chart-container">
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
|
||||
// 총건수 도넛 차트
|
||||
var totalDonutOption = {
|
||||
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||
title: {
|
||||
text: '총 건수 분포',
|
||||
left: 'center',
|
||||
@@ -99,7 +100,7 @@
|
||||
show: true
|
||||
},
|
||||
data: [
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||
]
|
||||
@@ -421,12 +422,13 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'API ID', 'API 명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
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: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
@@ -463,7 +465,7 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||
'Inbound Adapter', 'Outbound Adapter',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
@@ -510,11 +512,21 @@
|
||||
$("input[name=searchStartDateTime]").inputmask("9999-99", { 'autoUnmask': true });
|
||||
$("input[name=searchEndDateTime]").inputmask("9999-99", { 'autoUnmask': true });
|
||||
|
||||
// 기본값 설정 (최근 12개월)
|
||||
// 기본값 설정: 올해 1월 ~ 지난달 (현재가 1월이면 작년 1월 ~ 작년 12월)
|
||||
var today = new Date();
|
||||
var endMonth = today.getFullYear() + '-' + (today.getMonth() + 1 < 10 ? '0' : '') + (today.getMonth() + 1);
|
||||
var startDate = new Date(today.getFullYear(), today.getMonth() - 11, 1);
|
||||
var startMonth = startDate.getFullYear() + '-' + (startDate.getMonth() + 1 < 10 ? '0' : '') + (startDate.getMonth() + 1);
|
||||
var startDate, endDate;
|
||||
if (today.getMonth() === 0) { // 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()) {
|
||||
$("input[name=searchStartDateTime]").val(startMonth);
|
||||
@@ -593,7 +605,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API명</th>
|
||||
<th>API ID</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
@@ -632,10 +644,10 @@
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 요약 그리드 -->
|
||||
<!-- 요약 그리드 -->
|
||||
<div style="margin-top: 20px;">
|
||||
<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">
|
||||
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
||||
</button>
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
|
||||
// 총건수 도넛 차트
|
||||
var totalDonutOption = {
|
||||
color: ['#5470C6', '#FAC858', '#EE6666'],
|
||||
title: {
|
||||
text: '총 건수 분포',
|
||||
left: 'center',
|
||||
@@ -99,7 +100,7 @@
|
||||
show: true
|
||||
},
|
||||
data: [
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||
{ value: 0, name: '성공', itemStyle: { color: '#5470C6' } },
|
||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||
]
|
||||
@@ -397,12 +398,13 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'API ID', 'API 명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
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: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
@@ -439,7 +441,7 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||
'통계시간', 'API ID', '인스턴스', '업무구분', '클라이언트ID',
|
||||
'Inbound Adapter', 'Outbound Adapter',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
@@ -563,7 +565,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>API명</th>
|
||||
<th>API ID</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
@@ -604,7 +606,7 @@
|
||||
<!-- 요약 그리드 -->
|
||||
<div style="margin-top: 20px;">
|
||||
<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">
|
||||
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
||||
</button>
|
||||
|
||||
@@ -15,6 +15,19 @@
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
||||
<style>
|
||||
#gbox_grid .ui-jqgrid-ftable tr.footrow td {
|
||||
height: 26px;
|
||||
}
|
||||
.cssbtn.small {
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
min-width: 24px;
|
||||
box-shadow: none;
|
||||
border-color: #ebebec;
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.json"/>';
|
||||
var url_view = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.view"/>';
|
||||
@@ -60,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() {
|
||||
var postData = getPostData("cmd", "LIST");
|
||||
if (postData) {
|
||||
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
|
||||
applyGridColumnsByType($('input[name="searchType"]:checked').val());
|
||||
$("#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() {
|
||||
|
||||
var postData = getPostData("cmd", "EXCEL_EXPORT");
|
||||
@@ -181,12 +234,13 @@
|
||||
mtype: 'POST',
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'구분',
|
||||
'총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류',
|
||||
'제휴사', 'API명',
|
||||
'총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
colModel: [
|
||||
{ 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: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||
{ name: 'successRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||
@@ -198,19 +252,18 @@
|
||||
{ name: 'maxRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false }
|
||||
],
|
||||
jsonReader: { repeatitems: false },
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: 'auto',
|
||||
height: 500,
|
||||
rowNum: 10000,
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList: eval('[${rmsDefaultRowList}]'),
|
||||
footerrow: true,
|
||||
userDataOnFooter: true,
|
||||
loadComplete: function(d) {
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
|
||||
$(".ui-jqgrid-ftable td").css({ "font-weight": "bold", "background-color": "#f5f5f5" });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -221,10 +274,23 @@
|
||||
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
|
||||
|
||||
// 기본값 설정: 이번달 1일 ~ 어제 (오늘이 1일이면 지난달 1일 ~ 지난달 말일)
|
||||
var today = getToday();
|
||||
var startDate = today.substring(0,6)+"01";
|
||||
var endDate = today;
|
||||
|
||||
var now = new Date();
|
||||
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()) {
|
||||
$("input[name=searchStartDate]").val(startDate);
|
||||
@@ -242,6 +308,14 @@
|
||||
search();
|
||||
});
|
||||
|
||||
$("#btnPrevMonth").click(function() {
|
||||
shiftMonthRange(-1);
|
||||
});
|
||||
|
||||
$("#btnNextMonth").click(function() {
|
||||
shiftMonthRange(1);
|
||||
});
|
||||
|
||||
$("input[name=searchType]").click(function() {
|
||||
search();
|
||||
});
|
||||
@@ -284,6 +358,8 @@
|
||||
<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;">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -298,17 +374,16 @@
|
||||
<td>
|
||||
<input type="text" name="searchOrgName" id="searchOrgName" value="${param.searchOrgName}">
|
||||
</td>
|
||||
<th style="width:120px;">API명</th>
|
||||
<th style="width:120px;">API ID</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiName" id="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title>인터페이스 배포</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/transaction/apim/apiInterfaceMan.json"/>';
|
||||
var url_loader = '<c:url value="/loader.do"/>';
|
||||
|
||||
function loadList() {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: {cmd: 'LIST_DEPLOY_FILES'},
|
||||
success: function (json) {
|
||||
console.log('list', json)
|
||||
if (json) {
|
||||
$('#grid').jqGrid('clearGridData').jqGrid('setGridParam', {data: json.rows || []}).trigger('reloadGrid');
|
||||
}
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function callDeploy(filePath) {
|
||||
var postData = [];
|
||||
postData.push({name: "cmd", value: "interface"});
|
||||
postData.push({name: "control", value: "uploadfull"});
|
||||
postData.push({name: "filePath", value: filePath});
|
||||
return new Promise(function (resolve, reject) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url_loader,
|
||||
data: postData,
|
||||
success: function (response) {
|
||||
resolve(response);
|
||||
},
|
||||
error: function (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$("#btn_close").click(function () {
|
||||
window.close();
|
||||
});
|
||||
|
||||
$('#grid').jqGrid({
|
||||
datatype: "local",
|
||||
colNames: ['업무그룹', '인터페이스', '생성일시', 'filePath'],
|
||||
colModel: [
|
||||
{name: 'bizCode', align: 'center', width: '80', sortable: true},
|
||||
{name: 'interfaceId', align: 'left', sortable: true},
|
||||
{name: 'createdDate', align: 'center', width: '130', sortable: true},
|
||||
{name: 'filePath', hidden: true}
|
||||
],
|
||||
pager: $('#pager'),
|
||||
rowNum: 20,
|
||||
rowList: [10, 20, 50, 100],
|
||||
autoheight: false,
|
||||
height: 460,
|
||||
width: 650,
|
||||
autowidth: false,
|
||||
viewrecords: true,
|
||||
shrinkToFit: true,
|
||||
multiselect: true,
|
||||
multiboxonly: false
|
||||
});
|
||||
|
||||
loadList();
|
||||
|
||||
$("#btn_deploy").click(async function () {
|
||||
var rowIds = $('#grid').jqGrid('getGridParam', 'selarrrow');
|
||||
if (rowIds.length === 0) {
|
||||
alert('배포할 인터페이스를 선택하세요.');
|
||||
return;
|
||||
}
|
||||
if (!confirm(rowIds.length + '건을 배포 하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$("#btn_deploy").prop("disabled", true);
|
||||
|
||||
var successCount = 0;
|
||||
var failedList = [];
|
||||
for (var i = 0; i < rowIds.length; i++) {
|
||||
var rowData = $("#grid").getRowData(rowIds[i]);
|
||||
var target = rowData['bizCode'] + '/' + rowData['interfaceId'];
|
||||
try {
|
||||
var response = await callDeploy(rowData['filePath']);
|
||||
if (response != null && response.includes('status=success')) {
|
||||
successCount++;
|
||||
} else {
|
||||
failedList.push(target);
|
||||
}
|
||||
} catch (error) {
|
||||
failedList.push(target);
|
||||
}
|
||||
}
|
||||
|
||||
$("#btn_deploy").prop("disabled", false);
|
||||
|
||||
var msg = '배포 완료 - 성공[' + successCount + ']건, 실패[' + failedList.length + ']건';
|
||||
if (failedList.length > 0) {
|
||||
msg += '\n실패목록 : ' + failedList.join(', ');
|
||||
}
|
||||
alert(msg);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup_box">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_deploy" level="W"><i class="material-icons">cloud_upload</i> 배포
|
||||
</button>
|
||||
<button type="button" class="cssbtn" id="btn_close" level="R"><i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="title">배포 대상 인터페이스 목록</div>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ page import="com.eactive.eai.common.util.SystemUtil" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
@@ -37,6 +38,13 @@
|
||||
success: function (json) {
|
||||
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 설정
|
||||
putSelectFromParam();
|
||||
|
||||
@@ -85,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() {
|
||||
detail()
|
||||
var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로
|
||||
@@ -100,6 +119,7 @@
|
||||
'요청',
|
||||
'응답',
|
||||
'상태',
|
||||
'스펙 상태',
|
||||
'작성자',
|
||||
'가상응답여부',
|
||||
'변경일시(*)',
|
||||
@@ -112,6 +132,7 @@
|
||||
{name: 'fromAdapter', 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: 'specStatus', align: 'center', width: '40', formatter: specStatusFormatter, sortable: false, title: false},
|
||||
{name: 'author', align: 'center', width: '40', sortable: false},
|
||||
{name : 'simYn', align : 'center' , width:'40', hidden: true },
|
||||
{name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true},
|
||||
@@ -246,6 +267,11 @@
|
||||
window.alert('이관 대상 API 성공['+successCount+']건, 실패['+failedCount+']건 서버에 다운로드 완료');
|
||||
});
|
||||
|
||||
$("#btn_deploy").click(function(){
|
||||
var url2 = '<c:url value="/onl/transaction/apim/apiInterfaceMan.view"/>?cmd=DEPLOY_POPUP';
|
||||
showModal2(url2, {}, 663, 600);
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
|
||||
// 25.11.25 파일 가져오기 할 때 중복 검사 기능추가.
|
||||
@@ -380,7 +406,8 @@
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_download" level="W" status="DETAIL"><i class="material-icons">cloud_download</i> 이관</button>
|
||||
<button type="button" class="cssbtn" id="btn_download" level="W" status="DETAIL"><i class="material-icons">cloud_download</i> 이관</button>
|
||||
<!-- <button type="button" class="cssbtn" id="btn_deploy" level="W" status="DETAIL"><i class="material-icons">cloud_upload</i> 배포</button> -->
|
||||
<button type="button" class="cssbtn" id="btn_import" level="R" data-bs-toggle="modal"
|
||||
data-bs-target="#uploadModal"><i
|
||||
class="material-icons">upload</i> <%=localeMessage.getString("button.import")%>
|
||||
@@ -396,16 +423,10 @@
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:180px;">업무구분명</th>
|
||||
<td>
|
||||
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"></select>
|
||||
</td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}">
|
||||
@@ -415,6 +436,49 @@
|
||||
<input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:180px;">업무구분명</th>
|
||||
<td>
|
||||
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}"></select>
|
||||
</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>
|
||||
<td>
|
||||
<select name="searchStatusCode" style="width: 100%">
|
||||
<option value="">전체</option>
|
||||
<option value="N">정상</option>
|
||||
<option value="D">지연</option>
|
||||
<option value="E">장애</option>
|
||||
<option value="C">점검</option>
|
||||
</select>
|
||||
</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="help-text" style="color:#666; font-size:12px; margin-top:5px;">(*) : 정렬가능항목</div>
|
||||
@@ -449,4 +513,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -646,7 +646,7 @@
|
||||
const svcDesc = encodeURI(encodeURIComponent($('#eaiSvcDesc').val()));
|
||||
const cnvsnDesc = encodeURI(encodeURIComponent(transformDesc));
|
||||
var args = {title:transformName+' 수정', parent: true};
|
||||
var url='<c:url value="/onl/admin/rule/transform2/transform2Man.view"/>';
|
||||
var url='<c:url value="/onl/admin/rule/transformMan.view"/>';
|
||||
url += '?cmd=DETAIL';
|
||||
url += '&cnvsnName='+transformName;
|
||||
url += '&eaiSvcName='+svcName;
|
||||
@@ -793,7 +793,7 @@
|
||||
function updateFirstLayout(newTransformName, transformDesc){
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url: '<c:url value="/onl/admin/rule/transform2/transform2Man.json" />',
|
||||
url: '<c:url value="/onl/admin/rule/transformMan.json" />',
|
||||
data: { cmd: 'UPDATE',
|
||||
gridData: '[]',
|
||||
eaiSvcName: svcName,
|
||||
@@ -824,7 +824,7 @@
|
||||
};
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url: '<c:url value="/onl/admin/rule/transform2/transform2Man.json" />',
|
||||
url: '<c:url value="/onl/admin/rule/transformMan.json" />',
|
||||
data: { cmd: 'INSERT',
|
||||
gridData: '[]',
|
||||
cnvsnType,
|
||||
@@ -1608,8 +1608,10 @@
|
||||
<button type="button" class="cssbtn" id="btn_json_export" level="R" status="DETAIL,NEW"><i class="material-icons">download</i> <%=localeMessage.getString("button.exportJson")%></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>
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="R" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
|
||||
<!-- v1: 기존 API 스펙 (모달) — 기능 비교용 복원 -->
|
||||
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL"><i class="material-icons">description</i> API 스펙(삭제예정)</button>
|
||||
<%-- v1: 기존 API 스펙 (모달). PTL_PROPERTY(Portal / djb.apispec.v1.enabled) 로 노출 제어. --%>
|
||||
<c:if test="${apiSpecV1Enabled}">
|
||||
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL"><i class="material-icons">description</i> API 스펙</button>
|
||||
</c:if>
|
||||
<!-- v2: 신규 OpenAPI Spec (새 탭) -->
|
||||
<button type="button" class="cssbtn" id="btn_openapi_spec" level="R" status="DETAIL"><i class="material-icons">api</i> OpenAPI Spec</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>
|
||||
|
||||
@@ -55,10 +55,10 @@ $(document).ready(function() {
|
||||
'200',
|
||||
'300',
|
||||
'400',
|
||||
'900',
|
||||
/* '900', */
|
||||
'300',
|
||||
'400',
|
||||
'900',
|
||||
/* '900', */
|
||||
'IF',
|
||||
'<%= localeMessage.getString("tranStat.recv") %>',
|
||||
'<%= localeMessage.getString("common.total") %>',
|
||||
@@ -75,11 +75,11 @@ $(document).ready(function() {
|
||||
{ name : 'E200' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
|
||||
{ name : 'E300' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
|
||||
{ name : 'E400' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
|
||||
{ name : 'E900' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
|
||||
/* { name : 'E900' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, */
|
||||
|
||||
{ name : 'E9300' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
|
||||
{ name : 'E9400' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
|
||||
{ name : 'E9900' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'},
|
||||
/* { name : 'E9900' , align : 'center' , width:'50' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'sum'}, */
|
||||
|
||||
{ name : 'AVGEAI' , align : 'center' , width:'46' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'avg'},
|
||||
{ name : 'AVGPSV' , align : 'center' , width:'46' ,formatter:'integer',formatoptions:{thousandsSeparator:","},summaryType:'avg'},
|
||||
@@ -152,8 +152,8 @@ $(document).ready(function() {
|
||||
useColSpanStyle: true,
|
||||
groupHeaders:[
|
||||
{startColumnName: 'P100', numberOfColumns: 4, titleText: '<%= localeMessage.getString("tranStat.tranNo") %>'},
|
||||
{startColumnName: 'E100', numberOfColumns: 5, titleText: '<%= localeMessage.getString("tranStat.errNo") %>'},
|
||||
{startColumnName: 'E9300', numberOfColumns: 3, titleText: '<%= localeMessage.getString("tranStat.timeout") %>'},
|
||||
{startColumnName: 'E100', numberOfColumns: 4, titleText: '<%= localeMessage.getString("tranStat.errNo") %>'},
|
||||
{startColumnName: 'E9300', numberOfColumns: 2, titleText: '<%= localeMessage.getString("tranStat.timeout") %>'},
|
||||
{startColumnName: 'AVGEAI', numberOfColumns: 3, titleText: '<%= localeMessage.getString("tranStat.avgTime") %>'}
|
||||
]
|
||||
});
|
||||
@@ -210,9 +210,9 @@ $(document).ready(function() {
|
||||
var merge = new Array();
|
||||
merge.push(makeMerge("업무구분","0","1","0","0"));
|
||||
merge.push(makeMerge("구간별 처리건수","0","0","1","4"));
|
||||
merge.push(makeMerge("구간별 에러건수","0","0","5","9"));
|
||||
merge.push(makeMerge("타임아웃","0","0","10","12"));
|
||||
merge.push(makeMerge("평균처리시간(초)","0","0","13","15"));
|
||||
merge.push(makeMerge("구간별 에러건수","0","0","5","8"));
|
||||
merge.push(makeMerge("타임아웃","0","0","9","11"));
|
||||
merge.push(makeMerge("평균처리시간(초)","0","0","12","14"));
|
||||
|
||||
gridToExcelSubmit(url,"LIST_GRID_TO_EXCEL",$("#grid"),$("#ajaxForm"),"업무별 거래현황",merge);
|
||||
return false;
|
||||
@@ -224,9 +224,9 @@ $(document).ready(function() {
|
||||
var merge = new Array();
|
||||
merge.push(makeMerge("업무구분","0","1","0","0"));
|
||||
merge.push(makeMerge("구간별 처리건수","0","0","1","4"));
|
||||
merge.push(makeMerge("구간별 에러건수","0","0","5","9"));
|
||||
merge.push(makeMerge("타임아웃","0","0","10","12"));
|
||||
merge.push(makeMerge("평균처리시간(초)","0","0","13","15"));
|
||||
merge.push(makeMerge("구간별 에러건수","0","0","5","8"));
|
||||
merge.push(makeMerge("타임아웃","0","0","9","11"));
|
||||
merge.push(makeMerge("평균처리시간(초)","0","0","12","14"));
|
||||
gridToExcelSubmit(url_excel,"LIST_GRID_TO_EXCEL",$("#grid"),$("#ajaxForm"),'업무별 거래현황',merge);
|
||||
return false;
|
||||
});
|
||||
@@ -273,7 +273,6 @@ $(document).ready(function() {
|
||||
<%-- <img src="<c:url value="/img/btn_10min.png" />" alt="" id="btn_10min" level="W" /> --%>
|
||||
<button type="button" class="cssbtn" id="btn_detail" level="R" ><i class="material-icons">details</i> <%= localeMessage.getString("button.detail") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_excel" level="W" ><i class="material-icons">table_view</i> <%= localeMessage.getString("button.excel") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_initialize" level="W" ><i class="material-icons">restart_alt</i> <%= localeMessage.getString("button.reset") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
<%-- <img src="<c:url value="/img/btn_detail.png" />" alt="" id="btn_detail" level="R" /> --%>
|
||||
<%-- <img src="<c:url value="/img/btn_excel.png" />" alt="" id="btn_excel" level="W" /> --%>
|
||||
@@ -282,6 +281,9 @@ $(document).ready(function() {
|
||||
<%-- <img id="btn_excel" src="<c:url value="/img/btn_excel.png"/>" level="W" status="DETAIL"/> --%>
|
||||
</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>
|
||||
<table id="grid" ></table>
|
||||
</ajax>
|
||||
|
||||
@@ -59,10 +59,10 @@ function w2GridToExcelSubmit(url,cmd,gridObj,formObject,fileName,merge){
|
||||
records[i]['E200'] = data[i]['E200'].toString();
|
||||
records[i]['E300'] = data[i]['E300'].toString();
|
||||
records[i]['E400'] = data[i]['E400'].toString();
|
||||
records[i]['E900'] = data[i]['E900'].toString();
|
||||
/* records[i]['E900'] = data[i]['E900'].toString(); */
|
||||
records[i]['E9300'] = data[i]['E9300'].toString();
|
||||
records[i]['E9400'] = data[i]['E9400'].toString();
|
||||
records[i]['E9900'] = data[i]['E9900'].toString();
|
||||
/* records[i]['E9900'] = data[i]['E9900'].toString(); */
|
||||
|
||||
records[i]['BIZNAME'] = data[i]['BIZNAME'];
|
||||
records[i]['SVCCODE'] = data[i]['SVCCODE'];
|
||||
@@ -245,8 +245,8 @@ $( document ).ready(function() {
|
||||
{span : 4, caption:'<%= localeMessage.getString("tranStat.interface") %>'},
|
||||
{span : 4, caption:'<%= localeMessage.getString("tranStat.tranNo") %>'},
|
||||
{span : 3, caption:'<%= localeMessage.getString("tranStat.avgTime") %>'},
|
||||
{span : 5, caption:'<%= localeMessage.getString("tranStat.errNo") %>'},
|
||||
{span : 3, caption:'<%= localeMessage.getString("tranStat.timeout") %>'}
|
||||
{span : 4, caption:'<%= localeMessage.getString("tranStat.errNo") %>'},
|
||||
{span : 2, caption:'<%= localeMessage.getString("tranStat.timeout") %>'}
|
||||
],
|
||||
|
||||
columns: [
|
||||
@@ -269,11 +269,11 @@ $( document ).ready(function() {
|
||||
{ field : 'E200' ,type:'text' ,caption : '200' , size: '65px' ,style:'text-align:right', render:thousandsSeparator },
|
||||
{ field : 'E300' ,type:'text' ,caption : '300' , size: '65px' ,style:'text-align:right', render:thousandsSeparator },
|
||||
{ field : 'E400' ,type:'text' ,caption : '400' , size: '65px' ,style:'text-align:right', render:thousandsSeparator },
|
||||
{ field : 'E900' ,type:'text' ,caption : '900' , size: '65px' ,style:'text-align:right', render:thousandsSeparator },
|
||||
/* { field : 'E900' ,type:'text' ,caption : '900' , size: '65px' ,style:'text-align:right', render:thousandsSeparator }, */
|
||||
|
||||
{ field : 'E9300' ,type:'text' ,caption : '300' , size: '50px' ,style:'text-align:right', render:thousandsSeparator },
|
||||
{ field : 'E9400' ,type:'text' ,caption : '400' , size: '50px' ,style:'text-align:right', render:thousandsSeparator },
|
||||
{ field : 'E9900' ,type:'text' ,caption : '900' , size: '50px' ,style:'text-align:right', render:thousandsSeparator }
|
||||
/* { field : 'E9900' ,type:'text' ,caption : '900' , size: '50px' ,style:'text-align:right', render:thousandsSeparator } */
|
||||
]
|
||||
|
||||
,summary:[
|
||||
@@ -346,8 +346,8 @@ $( document ).ready(function() {
|
||||
merge.push(makeMerge("API","0","0","0","3"));
|
||||
merge.push(makeMerge("전문구간별 처리건수","0","0","4","7"));
|
||||
merge.push(makeMerge("평균처리시간(초)","0","0","8","10"));
|
||||
merge.push(makeMerge("전문구간별 에러건수","0","0","11","15"));
|
||||
merge.push(makeMerge("타임아웃","0","0","16","18"));
|
||||
merge.push(makeMerge("전문구간별 에러건수","0","0","11","14"));
|
||||
merge.push(makeMerge("타임아웃","0","0","15","16"));
|
||||
|
||||
w2GridToExcelSubmit(url_excel,"LIST_GRID_TO_EXCEL",w2ui['myGrid'],$("#ajaxForm"),"전문별 거래현황",merge);
|
||||
return false;
|
||||
@@ -376,8 +376,8 @@ $( document ).ready(function() {
|
||||
merge.push(makeMerge("API","0","0","0","3"));
|
||||
merge.push(makeMerge("구간별 처리건수","0","0","4","7"));
|
||||
merge.push(makeMerge("평균처리시간(초)","0","0","8","10"));
|
||||
merge.push(makeMerge("구간별 에러건수","0","0","11","15"));
|
||||
merge.push(makeMerge("타임아웃","0","0","16","18"));
|
||||
merge.push(makeMerge("구간별 에러건수","0","0","11","14"));
|
||||
merge.push(makeMerge("타임아웃","0","0","15","16"));
|
||||
|
||||
w2GridToExcelSubmit(url_excel,"LIST_GRID_TO_EXCEL",w2ui['myGrid'],$("#ajaxForm"),"전문별 거래현황",merge);
|
||||
return false;
|
||||
@@ -429,7 +429,6 @@ $( document ).ready(function() {
|
||||
<input type="checkbox" id="recentYnVal" value="N"/> <b><%= localeMessage.getString("tranStat.rec10Min") %></b>
|
||||
<button type="button" class="cssbtn" id="btn_detail" level="R" ><i class="material-icons">details</i> <%= localeMessage.getString("button.detail") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_excel" level="W" ><i class="material-icons">table_view</i> <%= localeMessage.getString("button.excel") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_initialize" level="W" ><i class="material-icons">restart_alt</i> <%= localeMessage.getString("button.reset") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
<%-- <img src="<c:url value="/img/btn_10min.png"/>" alt="" id="btn_10min" level="W" /> --%>
|
||||
<%-- <img src="<c:url value="/img/btn_detail.png"/>" alt="" id="btn_detail" level="R" /> --%>
|
||||
@@ -438,6 +437,9 @@ $( document ).ready(function() {
|
||||
<%-- <img src="<c:url value="/img/btn_search.png"/>" alt="" id="btn_search" level="R" /> --%>
|
||||
</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;>
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
var roleString = "<%=SessionManager.getRoleIdString(request)%>";
|
||||
let totalLogs = 0;
|
||||
let displayedLogs = 0;
|
||||
var bizErrCodeSet = new Set(); // ERRCODE_BIZ(업무 오류) 코드 목록 - 그리드 배경색 구분용
|
||||
|
||||
function adminChk() {
|
||||
var arrRoleID = roleString.split(',');
|
||||
@@ -106,6 +107,8 @@
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchExtBizCd]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.extBizList).setFormat(codeName3OptionFormat).rendering();
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchLogPrcssSerno]")).setNoValueInclude(true).setNoValue("", "전체").setData(json.logPrcssSernoList).rendering();
|
||||
|
||||
bizErrCodeSet = new Set((json.bizErrCodeList || []).map(function (c) { return c.CODE; }));
|
||||
|
||||
setSearchable("searchEaiBzwkDstcd"); // 콤보에 searchable 설정
|
||||
setSearchable("searchExtBizCd");
|
||||
|
||||
@@ -222,63 +225,6 @@
|
||||
$("input[name=searchEndHHMM]").val(endHHMM);
|
||||
}
|
||||
|
||||
function guidEvent() {
|
||||
var searchGuid = $("input[name=searchGuid]").val();
|
||||
if (searchGuid.trim() == '')
|
||||
return;
|
||||
|
||||
var searchTm = searchGuid.trim();
|
||||
/*
|
||||
var yy = searchTm.substr(13,4);
|
||||
var mm = searchTm.substr(17,2);
|
||||
var dd = searchTm.substr(19,2);
|
||||
var hh = searchTm.substr(21,2);
|
||||
var MM = searchTm.substr(23,2);
|
||||
*/
|
||||
var yy = searchTm.substr(3,4);
|
||||
var mm = searchTm.substr(7,2);
|
||||
var dd = searchTm.substr(9,2);
|
||||
var hh = searchTm.substr(11,2);
|
||||
var MM = searchTm.substr(13,2);
|
||||
|
||||
var ms = Date.UTC(yy,mm-1,dd,hh,MM,00);
|
||||
var date = new Date(ms);
|
||||
var startYYYYMMDD;
|
||||
var startHHMM;
|
||||
var endYYYYMMDD;
|
||||
var endHHMM;
|
||||
//Local Time Setting
|
||||
date.setHours(date.getHours() - 9);
|
||||
//5분 간격
|
||||
date.setMinutes(date.getMinutes() - 5);
|
||||
|
||||
startYYYYMMDD = date.getFullYear() + right("0" + (date.getMonth() + 1), 2) + right("0" + (date.getDate()), 2);
|
||||
startHHMM = right("0" + date.getHours(), 2) + right("0" + date.getMinutes(), 2) + "00";
|
||||
|
||||
date.setMinutes(date.getMinutes() + 10);
|
||||
|
||||
endYYYYMMDD = date.getFullYear() + right("0" + (date.getMonth() + 1), 2) + right("0" + (date.getDate()), 2);
|
||||
endHHMM = right("0" + date.getHours(), 2) + right("0" + date.getMinutes(), 2) + "00";
|
||||
|
||||
//추가 코드 입력 2012.05.16 권세환 (일자가 바뀔경우 시간 간격을 +-5분 -> +-1분)
|
||||
if (startYYYYMMDD.substr(6, 2) != endYYYYMMDD.substr(6, 2)) {
|
||||
date.setMinutes(date.getMinutes() - 6);
|
||||
|
||||
startYYYYMMDD = date.getFullYear() + right("0" + (date.getMonth() + 1), 2) + right("0" + (date.getDate()), 2);
|
||||
startHHMM = right("0" + date.getHours(), 2) + right("0" + date.getMinutes(), 2) + "00";
|
||||
|
||||
date.setMinutes(date.getMinutes() + 2);
|
||||
|
||||
endYYYYMMDD = date.getFullYear() + right("0" + (date.getMonth() + 1), 2) + right("0" + (date.getDate()), 2);
|
||||
endHHMM = right("0" + date.getHours(), 2) + right("0" + date.getMinutes(), 2) + "00";
|
||||
}
|
||||
$("input[name=searchStartYYYYMMDD]").val(startYYYYMMDD);
|
||||
$("input[name=searchEndYYYYMMDD]").val(endYYYYMMDD);
|
||||
$("input[name=searchStartHHMM]").val(startHHMM);
|
||||
$("input[name=searchEndHHMM]").val(endHHMM);
|
||||
|
||||
}
|
||||
|
||||
function timeStampFormat2(cellvalue, options, rowObject) {
|
||||
if (cellvalue == null || cellvalue.length < 17)
|
||||
return '';
|
||||
@@ -368,7 +314,8 @@
|
||||
'<%=localeMessage.getString("tracking.companyitcontactname")%>',
|
||||
'<%=localeMessage.getString("tracking.companyitcontacttelno")%>',
|
||||
'<%=localeMessage.getString("tracking.itcontactpointperson1")%>',
|
||||
'<%=localeMessage.getString("tracking.bzwkcontactperson")%>'
|
||||
'<%=localeMessage.getString("tracking.bzwkcontactperson")%>',
|
||||
'EAIERRCD'
|
||||
],
|
||||
colModel: [
|
||||
{name: 'EAISVCSERNO', hidden: true},
|
||||
@@ -398,7 +345,8 @@
|
||||
{name: 'COMPANYITCONTACTNAME', align: 'left', width: '110', hidden: true},
|
||||
{name: 'COMPANYITCONTACTTELNO', align: 'left', width: '120', hidden: true},
|
||||
{name: 'ITCONTACTPOINTPERSON1', align: 'left', width: '120', hidden: true},
|
||||
{name: 'BZWKCONTACTPERSON', align: 'left', width: '120', hidden: true}
|
||||
{name: 'BZWKCONTACTPERSON', align: 'left', width: '120', hidden: true},
|
||||
{name: 'EAIERRCD', hidden: true}
|
||||
],
|
||||
jsonReader: {
|
||||
repeatitems: false
|
||||
@@ -435,7 +383,7 @@
|
||||
|
||||
},
|
||||
loadComplete: function (d) {
|
||||
$("#grid").tuiTableRowSpan("7");
|
||||
|
||||
|
||||
// 거래 건수 표시
|
||||
var uniqueCount = calculateUniqueLogCount("#grid");
|
||||
@@ -444,14 +392,22 @@
|
||||
//에러 행 색상 표시
|
||||
var rows = $(this).getDataIDs();
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var val = $("#grid").getCell(rows[i], 'ERRYN');
|
||||
if (val == "Y") {
|
||||
var errYn = $("#grid").getCell(rows[i], 'ERRYN');
|
||||
var eaiErrCd = $("#grid").getCell(rows[i], 'EAIERRCD');
|
||||
var isBizErr = eaiErrCd != null && bizErrCodeSet.has(String(eaiErrCd).trim());
|
||||
|
||||
if (isBizErr) {
|
||||
// 업무(BIZ) 오류코드(ERRCODE_BIZ) - 하늘색 배경
|
||||
$("#" + rows[i]).find("td").css("background-color", "#BEEDFF");
|
||||
} else if (errYn == "Y") {
|
||||
$("#" + rows[i]).find("td").css("background-color", "#FFCC99");
|
||||
}
|
||||
}
|
||||
|
||||
$("#grid").tuiTableRowSpan("7");
|
||||
|
||||
//1000건이상 alert
|
||||
if (d.records == 1000) {
|
||||
if (d.records >= 1000) {
|
||||
setTimeout(() => {
|
||||
alert("<%=localeMessage.getString("tracking.alert5")%>"); // 조회 결과 1,000건 초과
|
||||
}, 0); // Alert는 비동기적으로 처리
|
||||
@@ -481,7 +437,6 @@
|
||||
//$("#grid").jqGrid('setFrozenColumns');
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
$("input[name=searchGuid]").blur();
|
||||
detail();
|
||||
getTransactionCounts();
|
||||
});
|
||||
@@ -551,12 +506,6 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
//GUID
|
||||
$("input[name=searchGuid]").blur(function (event) {
|
||||
guidEvent();
|
||||
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
|
||||
var local = localStorage.getItem("guid");
|
||||
@@ -701,7 +650,7 @@
|
||||
<td colspan="3"><input type="text" name="searchEaiSvcSerno" value="${param.searchEaiSvcSerno}"></td>
|
||||
|
||||
<th><%=localeMessage.getString("tracking.keymgtmsgctnt")%></th>
|
||||
<td colspan="2"><input type="text" name="searchKeyMgtMsgCtnt" value="${param.searchKeyMgtMsgCtnt}"></td>
|
||||
<td colspan="2"><input type="text" name="searchGuid" value="${param.searchGuid}" ></td>
|
||||
|
||||
<%-- <th><%=localeMessage.getString("tracking.orgGUID")%></th>
|
||||
<td colspan="4"><input type="text" name="searchGuid" value="" maxlength="<%=DbTrackingService.GUID_LENGTH%>"></td> --%>
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.table_row td { padding: 0 5px; }
|
||||
.table_row input { box-sizing: border-box; }
|
||||
|
||||
</style>
|
||||
<script language="javascript" >
|
||||
// var eaiBzwkDstcd = window.dialogArguments["eaiBzwkDstcd"];
|
||||
@@ -253,7 +256,7 @@
|
||||
var name = $(this).attr("name");
|
||||
if ("${rmsMenuAuth}" =="W"){ //2020.07.08 admin 만 버튼보이게 수정
|
||||
$("#"+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");
|
||||
if($("input[name="+name+"]").val().trim() != ""){
|
||||
$("#"+name).show();
|
||||
$("input[name="+name+"]").css("width","185px");
|
||||
$("input[name="+name+"]").css("width","calc(100% - 60px)");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -376,7 +379,7 @@
|
||||
const value = bodyCodeEditor.getValue();
|
||||
let prettyValue = value;
|
||||
if(syntax === 'JSN'){
|
||||
prettyValue = vkbeautify.json(value);
|
||||
prettyValue = preserveExponential(value); //vkbeautify.json(value);
|
||||
}else if(syntax === 'XML'){
|
||||
prettyValue = vkbeautify.xml(value).trim();
|
||||
}
|
||||
@@ -407,6 +410,19 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
function preserveExponential(jsonString) {
|
||||
// 1. 지수 형태의 숫자(예: 1e+21, 5.43e-7)를 찾아 문자열 ""로 감쌉니다.
|
||||
// JSON의 key 뒤에 오는 value 위치의 지수 표기법을 매칭합니다.
|
||||
let protectedJson = jsonString.replace(/:\s*([+-]?\d+\.?\d*e[+-]?\d+)/gi, ': "$1"');
|
||||
|
||||
// 2. 보호된 상태로 vkbeautify를 실행하여 예쁘게 정렬합니다.
|
||||
let prettyJson = vkbeautify.json(protectedJson);
|
||||
|
||||
// 3. (선택사항) 만약 최종 결과물에서 반드시 큰따옴표("")를 다시 제거해야 한다면 아래 주석을 해제하세요.
|
||||
prettyJson = prettyJson.replace(/:\s*"([+-]?\d+\.?\d*e[+-]?\d+)"/gi, ': $1');
|
||||
|
||||
return prettyJson;
|
||||
}
|
||||
function doChangeSyntax(syntax){
|
||||
if(syntax === 'JSN'){
|
||||
$('#btn_pretty_body').css('display', 'block');
|
||||
@@ -789,6 +805,35 @@ $(document).ready(function() {
|
||||
$("html, hbody").css("overflow-x","scroll");
|
||||
}
|
||||
|
||||
function copyTextToClipboard(text, successMessage){
|
||||
if (typeof(navigator.clipboard) == 'undefined'){
|
||||
var textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
textArea.style.position = "fixed";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
var successful = false;
|
||||
try{
|
||||
successful = document.execCommand('copy');
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
if (successful){
|
||||
alert(successMessage);
|
||||
}else{
|
||||
prompt("Ctrl+C 를 눌러 클립보드로 복사하세요.", text);
|
||||
}
|
||||
}else{
|
||||
navigator.clipboard.writeText(text).then(function(){
|
||||
alert(successMessage);
|
||||
}).catch(function(){
|
||||
prompt("클립보드 복사에 실패했습니다. Ctrl+C 를 눌러 복사하세요.", text);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$('#headerToClipboard').click(function(){
|
||||
if($('#grid1').get(0) == null || $('#grid1').get(0).jexcel == null){
|
||||
return;
|
||||
@@ -800,13 +845,11 @@ $(document).ready(function() {
|
||||
paddedValues += row[5].padEnd(row[4], " ");
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(paddedValues);//클립보드로 복사
|
||||
alert('헤더값이 복사되었습니다.')
|
||||
copyTextToClipboard(paddedValues, '헤더값이 복사되었습니다.');
|
||||
});
|
||||
|
||||
$('#messageToClipboard').click(function() {
|
||||
navigator.clipboard.writeText(fullMessageData);//클립보드로 복사
|
||||
alert('전체 전문 복사되었습니다.')
|
||||
copyTextToClipboard(fullMessageData, '전체 전문 복사되었습니다.');
|
||||
});
|
||||
|
||||
$('#btn_pretty_print').click(function () {
|
||||
@@ -937,15 +980,23 @@ html,hbody {
|
||||
<input type=hidden name=realHeader>
|
||||
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.common")%></div>
|
||||
<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>
|
||||
<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>
|
||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.eaiBzwkDstcd")%></th>
|
||||
<td style="width:30%;"><input type="text" name="eaiBzwkDstcd" readonly="readonly"/> </td>
|
||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.msgDpstYMS")%></th>
|
||||
<td style="width:30%;"><input type="text" name="msgDpstYMS" readonly="readonly"/> </td>
|
||||
<th><%=localeMessage.getString("trackingDetail.eaiBzwkDstcd")%></th>
|
||||
<td><input type="text" name="eaiBzwkDstcd" readonly="readonly"/> </td>
|
||||
<th><%=localeMessage.getString("trackingDetail.msgDpstYMS")%></th>
|
||||
<td><input type="text" name="msgDpstYMS" readonly="readonly"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th><td><input type="text" name="eaiSvcName" readonly="readonly" />
|
||||
@@ -979,11 +1030,17 @@ html,hbody {
|
||||
</table>
|
||||
<div class="table_row_title">API 정보</div>
|
||||
<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>
|
||||
<th style="width:20%;">APP(Client) ID</th>
|
||||
<td style="width:30%;"><input type="text" name="clientId" readonly="readonly"/> </td>
|
||||
<th style="width:20%;">APP 명</th>
|
||||
<td style="width:30%;"><input type="text" name="clientName" readonly="readonly"/> </td>
|
||||
<th>APP(Client) ID</th>
|
||||
<td><input type="text" name="clientId" readonly="readonly"/> </td>
|
||||
<th>APP 명</th>
|
||||
<td><input type="text" name="clientName" readonly="readonly"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>법인 ID</th><td ><input type="text" name="orgId" readonly="readonly"/> </td>
|
||||
@@ -1105,20 +1162,27 @@ html,hbody {
|
||||
</div><!-- end.container-1 -->
|
||||
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.inbound")%></div>
|
||||
<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>
|
||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.svcMotivUseDstcd")%></th>
|
||||
<td style="width:30%;"><input type="text" name="svcMotivUseDstcd" readonly="readonly"/> </td>
|
||||
<%-- <th style="width:20%;"><%=localeMessage.getString("trackingDetail.svcMotivUseDstcd")%></th>
|
||||
<td style="width:30%;"><input type="text" name="svcMotivUseDstcd" readonly="readonly"/> </td> --%>
|
||||
<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"/> --%>
|
||||
<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>
|
||||
</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>
|
||||
</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.svcBfClmnLogYn")%></th><td><input type="text" name="svcBfClmnLogYn" readonly="readonly"/> </td>
|
||||
</tr>
|
||||
@@ -1129,39 +1193,52 @@ html,hbody {
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</tr> --%>
|
||||
</table>
|
||||
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.outbound")%></div>
|
||||
<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>
|
||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvIntfacDsticName")%></th>
|
||||
<td style="width:30%;"><input type="text" name="psvIntfacDsticName" readonly="readonly"/> </td>
|
||||
<%-- <th style="width:20%;"><%=localeMessage.getString("trackingDetail.psvIntfacDsticName")%></th>--%>
|
||||
<%-- <td style="width:30%;"><input type="text" name="psvIntfacDsticName" readonly="readonly"/> </td> --%>
|
||||
<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"/> --%>
|
||||
<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>
|
||||
<th><%=localeMessage.getString("trackingDetail.psvBzwkSysName")%></th><td><input type="text" name="psvBzwkSysName" readonly="readonly"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<th><%=localeMessage.getString("trackingDetail.psvSysSvcDsticName")%></th><td><input type="text" name="psvSysSvcDsticName" readonly="readonly"/> </td>
|
||||
|
||||
</tr>--%>
|
||||
<%-- <tr>
|
||||
<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>
|
||||
</tr>
|
||||
</tr> --%>
|
||||
</table>
|
||||
<div class="table_row_title"><%=localeMessage.getString("trackingDetail.MAPPING")%></div>
|
||||
<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>
|
||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.chngYn")%></th>
|
||||
<td style="width:30%;"><input type="text" name="chngYn" readonly="readonly"/> </td>
|
||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.chngMsgIdName")%></th>
|
||||
<td style="width:30%;"><input type="text" name="chngMsgIdName" readonly="readonly"/>
|
||||
<th><%=localeMessage.getString("trackingDetail.chngYn")%></th>
|
||||
<td><input type="text" name="chngYn" readonly="readonly"/> </td>
|
||||
<th><%=localeMessage.getString("trackingDetail.chngMsgIdName")%></th>
|
||||
<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" /-->
|
||||
<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>
|
||||
@@ -1170,7 +1247,7 @@ html,hbody {
|
||||
<th><%=localeMessage.getString("trackingDetail.bascRspnsChngYn")%></th><td><input type="text" name="bascRspnsChngYn" readonly="readonly"/> </td>
|
||||
<th><%=localeMessage.getString("trackingDetail.bascRspnsChngMsgIdName")%></th>
|
||||
<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" /-->
|
||||
<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>
|
||||
@@ -1180,10 +1257,10 @@ html,hbody {
|
||||
<th><%=localeMessage.getString("trackingDetail.errRspnsChngYn")%></th><td><input type="text" name="errRspnsChngYn" readonly="readonly"/> </td>
|
||||
<th><%=localeMessage.getString("trackingDetail.errRspnsChngMsgIdName")%></th>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<%-- <tr>
|
||||
<th><%=localeMessage.getString("trackingDetail.inptMsgIDName")%></th>
|
||||
<td>
|
||||
<input type="text" name="inptMsgIDName" readonly="readonly" />
|
||||
@@ -1193,9 +1270,9 @@ html,hbody {
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</tr> --%>
|
||||
</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" >
|
||||
<tr>
|
||||
<th style="width:20%;"><%=localeMessage.getString("trackingDetail.rspnsErrcdName")%></th>
|
||||
@@ -1227,7 +1304,7 @@ html,hbody {
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
</table> --%>
|
||||
</form>
|
||||
</div><!-- end.popup_box -->
|
||||
<div id="bizLogObjectDialog" style="white-space: pre-wrap; overflow-y:auto" title="Biz Log Object Detail">
|
||||
|
||||
@@ -1682,7 +1682,7 @@ $(document).ready(function () {
|
||||
var args = new Object();
|
||||
var url = '<c:url value="/onl/admin/rule/layoutMan.view" />';
|
||||
url += "?cmd=DETAILPOPUP";
|
||||
url += '&loutName='+layoutName;
|
||||
url += '&searchLoutName='+layoutName;
|
||||
url += '&pop=true';
|
||||
|
||||
showModal2(url,args,890,940,'','scroll:on');
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%@
|
||||
page import="org.quartz.*,
|
||||
org.quartz.impl.matchers.GroupMatcher,
|
||||
org.springframework.web.context.WebApplicationContext,
|
||||
org.springframework.web.context.support.WebApplicationContextUtils,
|
||||
java.util.*,
|
||||
java.text.SimpleDateFormat"
|
||||
%><%!
|
||||
private String fmt(Date date) {
|
||||
if (date == null) return "-";
|
||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
|
||||
}
|
||||
|
||||
private String relative(Date date) {
|
||||
if (date == null) return "";
|
||||
long diff = date.getTime() - System.currentTimeMillis();
|
||||
String prefix = diff >= 0 ? "+" : "-";
|
||||
long abs = Math.abs(diff) / 1000;
|
||||
if (abs < 60) return prefix + abs + "s";
|
||||
if (abs < 3600) return prefix + (abs / 60) + "m " + (abs % 60) + "s";
|
||||
return prefix + (abs / 3600) + "h " + ((abs % 3600) / 60) + "m";
|
||||
}
|
||||
|
||||
private String stateClass(Trigger.TriggerState state) {
|
||||
if (state == Trigger.TriggerState.NORMAL) return "state-normal";
|
||||
if (state == Trigger.TriggerState.PAUSED) return "state-paused";
|
||||
if (state == Trigger.TriggerState.BLOCKED) return "state-blocked";
|
||||
if (state == Trigger.TriggerState.ERROR) return "state-error";
|
||||
return "";
|
||||
}
|
||||
|
||||
private Map<String, Object> buildTriggerInfo(Scheduler scheduler, Trigger trigger) throws Exception {
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
|
||||
info.put("triggerName", trigger.getKey().getName());
|
||||
info.put("triggerGroup", trigger.getKey().getGroup());
|
||||
info.put("state", state.name());
|
||||
info.put("stateClass", stateClass(state));
|
||||
info.put("prevFire", fmt(trigger.getPreviousFireTime()));
|
||||
info.put("prevFireRel", relative(trigger.getPreviousFireTime()));
|
||||
info.put("nextFire", fmt(trigger.getNextFireTime()));
|
||||
info.put("nextFireRel", relative(trigger.getNextFireTime()));
|
||||
info.put("priority", trigger.getPriority());
|
||||
if (trigger instanceof CronTrigger) {
|
||||
info.put("type", "Cron");
|
||||
info.put("cronExp", ((CronTrigger) trigger).getCronExpression());
|
||||
} else if (trigger instanceof SimpleTrigger) {
|
||||
SimpleTrigger st = (SimpleTrigger) trigger;
|
||||
info.put("type", "Simple");
|
||||
info.put("cronExp", "interval=" + st.getRepeatInterval() + "ms, repeat=" +
|
||||
(st.getRepeatCount() == SimpleTrigger.REPEAT_INDEFINITELY ? "INF" : st.getRepeatCount()));
|
||||
} else {
|
||||
info.put("type", trigger.getClass().getSimpleName());
|
||||
info.put("cronExp", "-");
|
||||
}
|
||||
if (trigger.getStartTime() != null) info.put("startTime", fmt(trigger.getStartTime()));
|
||||
if (trigger.getEndTime() != null) info.put("endTime", fmt(trigger.getEndTime()));
|
||||
return info;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> getJobList(Scheduler scheduler) throws Exception {
|
||||
List<Map<String, Object>> jobs = new ArrayList<>();
|
||||
for (String group : scheduler.getJobGroupNames()) {
|
||||
for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(group))) {
|
||||
JobDetail detail = scheduler.getJobDetail(jobKey);
|
||||
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
|
||||
|
||||
Map<String, Object> job = new LinkedHashMap<>();
|
||||
job.put("name", jobKey.getName());
|
||||
job.put("group", jobKey.getGroup());
|
||||
job.put("jobClass", detail.getJobClass().getSimpleName());
|
||||
job.put("disallowConcurrent", detail.isConcurrentExectionDisallowed());
|
||||
|
||||
StringBuilder dataStr = new StringBuilder();
|
||||
for (String key : detail.getJobDataMap().getKeys()) {
|
||||
if (dataStr.length() > 0) dataStr.append(", ");
|
||||
dataStr.append(key).append("=").append(detail.getJobDataMap().getString(key));
|
||||
}
|
||||
job.put("jobData", dataStr.toString());
|
||||
job.put("triggerCount", triggers.size());
|
||||
|
||||
List<Map<String, Object>> triggerList = new ArrayList<>();
|
||||
for (Trigger trigger : triggers) {
|
||||
triggerList.add(buildTriggerInfo(scheduler, trigger));
|
||||
}
|
||||
job.put("triggerList", triggerList);
|
||||
|
||||
if (!triggers.isEmpty()) {
|
||||
Trigger trigger = triggers.get(0);
|
||||
Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
|
||||
job.put("state", state.name());
|
||||
job.put("stateClass", stateClass(state));
|
||||
job.put("prevFire", fmt(trigger.getPreviousFireTime()));
|
||||
job.put("prevFireRel", relative(trigger.getPreviousFireTime()));
|
||||
job.put("nextFire", fmt(trigger.getNextFireTime()));
|
||||
job.put("nextFireRel", relative(trigger.getNextFireTime()));
|
||||
if (trigger instanceof CronTrigger) {
|
||||
job.put("cronExp", ((CronTrigger) trigger).getCronExpression());
|
||||
} else {
|
||||
job.put("cronExp", "-");
|
||||
}
|
||||
} else {
|
||||
job.put("state", "NO_TRIGGER");
|
||||
job.put("stateClass", "state-error");
|
||||
job.put("prevFire", "-");
|
||||
job.put("prevFireRel", "");
|
||||
job.put("nextFire", "-");
|
||||
job.put("nextFireRel", "");
|
||||
job.put("cronExp", "-");
|
||||
}
|
||||
jobs.add(job);
|
||||
}
|
||||
}
|
||||
jobs.sort(Comparator.comparing(m -> (String) m.get("name")));
|
||||
return jobs;
|
||||
}
|
||||
%><%
|
||||
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(application);
|
||||
|
||||
Scheduler scheduler = null;
|
||||
Scheduler clusteredScheduler = null;
|
||||
String error = null;
|
||||
|
||||
try {
|
||||
scheduler = (Scheduler) ctx.getBean("scheduler");
|
||||
} catch (Exception e) {
|
||||
error = "scheduler bean not found: " + e.getMessage();
|
||||
}
|
||||
try {
|
||||
clusteredScheduler = (Scheduler) ctx.getBean("clusteredScheduler");
|
||||
} catch (Exception e) {
|
||||
// clusteredScheduler is optional
|
||||
}
|
||||
|
||||
// 자동 새로고침 간격 (초), 기본 10초
|
||||
String refreshParam = request.getParameter("refresh");
|
||||
int refreshSec = 10;
|
||||
if (refreshParam != null) {
|
||||
try { refreshSec = Integer.parseInt(refreshParam); } catch (NumberFormatException e) { /* ignore */ }
|
||||
if (refreshSec < 1) refreshSec = 1;
|
||||
}
|
||||
boolean autoRefresh = refreshParam != null;
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<% if (autoRefresh) { %>
|
||||
<meta http-equiv="refresh" content="<%= refreshSec %>;url=quartzStatus.jsp?refresh=<%= refreshSec %>">
|
||||
<% } %>
|
||||
<title>Quartz Scheduler Status</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 20px; background: #f5f5f5; color: #333; }
|
||||
h1 { font-size: 20px; margin-bottom: 5px; }
|
||||
h2 { font-size: 16px; color: #555; margin: 20px 0 8px 0; border-bottom: 2px solid #ddd; padding-bottom: 4px; }
|
||||
.info { font-size: 13px; color: #666; margin-bottom: 15px; }
|
||||
table { width: 100%%; border-collapse: collapse; font-size: 13px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
||||
th { background: #4a5568; color: #fff; padding: 8px 10px; text-align: left; font-weight: 500; }
|
||||
td { padding: 7px 10px; border-bottom: 1px solid #eee; }
|
||||
tr:hover td { background: #f7fafc; }
|
||||
.state-normal { color: #38a169; font-weight: bold; }
|
||||
.state-paused { color: #d69e2e; font-weight: bold; }
|
||||
.state-blocked { color: #e53e3e; font-weight: bold; }
|
||||
.state-error { color: #e53e3e; font-weight: bold; }
|
||||
.mono { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
|
||||
.rel { color: #718096; font-size: 11px; margin-left: 4px; }
|
||||
.badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 11px; }
|
||||
.badge-yes { background: #c6f6d5; color: #276749; }
|
||||
.badge-no { background: #fed7d7; color: #9b2c2c; }
|
||||
.scheduler-meta { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 10px; }
|
||||
.scheduler-meta span { font-size: 13px; }
|
||||
.scheduler-meta strong { color: #4a5568; }
|
||||
.error { color: #e53e3e; padding: 10px; background: #fff5f5; border: 1px solid #fc8181; border-radius: 4px; }
|
||||
.toolbar { font-size: 12px; color: #718096; }
|
||||
.toolbar a { color: #4299e1; text-decoration: none; margin: 0 4px; }
|
||||
.toolbar a:hover { text-decoration: underline; }
|
||||
.toolbar .active { font-weight: bold; color: #2b6cb0; background: #ebf4ff; padding: 2px 6px; border-radius: 3px; }
|
||||
.auto-badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; background: #bee3f8; color: #2a4365; margin-left: 6px; }
|
||||
.trigger-toggle { cursor: pointer; user-select: none; }
|
||||
.trigger-toggle:hover td { background: #edf2f7; }
|
||||
.trigger-row { display: none; }
|
||||
.trigger-row.open { display: table-row; }
|
||||
.trigger-row td { background: #f7fafc; padding: 5px 10px 5px 30px; border-bottom: 1px solid #e2e8f0; font-size: 12px; }
|
||||
.trigger-detail-table { width: 100%; border-collapse: collapse; font-size: 12px; margin: 0; }
|
||||
.trigger-detail-table th { background: #e2e8f0; color: #4a5568; padding: 5px 8px; font-weight: 500; font-size: 11px; }
|
||||
.trigger-detail-table td { padding: 4px 8px; border-bottom: 1px solid #e2e8f0; }
|
||||
.trigger-detail-table tr:last-child td { border-bottom: none; }
|
||||
.toggle-icon { display: inline-block; width: 16px; font-size: 11px; color: #718096; transition: transform 0.15s; }
|
||||
.toggle-icon.open { transform: rotate(90deg); }
|
||||
.trigger-count { font-size: 11px; color: #718096; font-weight: normal; margin-left: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Quartz Scheduler Status
|
||||
<% if (autoRefresh) { %><span class="auto-badge">Auto <%= refreshSec %>s</span><% } %>
|
||||
</h1>
|
||||
<p class="info">
|
||||
Instance: <strong><%= System.getProperty("inst.Name", "N/A") %></strong>
|
||||
| Server Time: <strong><%= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) %></strong>
|
||||
</p>
|
||||
<p class="toolbar">
|
||||
Auto Refresh:
|
||||
<% if (!autoRefresh) { %><span class="active">OFF</span><% } else { %><a href="quartzStatus.jsp">OFF</a><% } %>
|
||||
|
|
||||
<% int[] intervals = {5, 10, 30, 60}; %>
|
||||
<% for (int iv : intervals) { %>
|
||||
<% if (autoRefresh && refreshSec == iv) { %><span class="active"><%= iv %>s</span><% } else { %><a href="quartzStatus.jsp?refresh=<%= iv %>"><%= iv %>s</a><% } %>
|
||||
<% } %>
|
||||
</p>
|
||||
|
||||
<% if (error != null) { %>
|
||||
<div class="error"><%= error %></div>
|
||||
<% } %>
|
||||
|
||||
<% if (scheduler != null) { %>
|
||||
<h2>Scheduler: <%= scheduler.getSchedulerName() %> (RAM)</h2>
|
||||
<div class="scheduler-meta">
|
||||
<span><strong>Status:</strong> <span class="<%= scheduler.isStarted() ? "state-normal" : "state-error" %>"><%= scheduler.isStarted() ? "STARTED" : "STOPPED" %></span></span>
|
||||
<span><strong>Instance ID:</strong> <%= scheduler.getSchedulerInstanceId() %></span>
|
||||
<span><strong>Job Count:</strong> <%= getJobList(scheduler).size() %></span>
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Job Name</th>
|
||||
<th>Job Class</th>
|
||||
<th>Cron</th>
|
||||
<th>State</th>
|
||||
<th>Concurrent</th>
|
||||
<th>Prev Fire</th>
|
||||
<th>Next Fire</th>
|
||||
<th>Job Data</th>
|
||||
</tr>
|
||||
<% int jobIdx = 0; for (Map<String, Object> job : getJobList(scheduler)) { String rowId = "s_" + jobIdx; List<Map<String, Object>> tList = (List<Map<String, Object>>) job.get("triggerList"); %>
|
||||
<tr class="trigger-toggle" onclick="toggleTriggers('<%= rowId %>')">
|
||||
<td><span class="toggle-icon" id="icon_<%= rowId %>">▶</span><strong><%= job.get("name") %></strong><span class="trigger-count">(<%= job.get("triggerCount") %> triggers)</span></td>
|
||||
<td class="mono"><%= job.get("jobClass") %></td>
|
||||
<td class="mono"><%= job.get("cronExp") %></td>
|
||||
<td class="<%= job.get("stateClass") %>"><%= job.get("state") %></td>
|
||||
<td><span class="badge <%= (Boolean)job.get("disallowConcurrent") ? "badge-yes" : "badge-no" %>">
|
||||
<%= (Boolean)job.get("disallowConcurrent") ? "NO (protected)" : "YES (allowed)" %></span></td>
|
||||
<td class="mono"><%= job.get("prevFire") %><span class="rel"><%= job.get("prevFireRel") %></span></td>
|
||||
<td class="mono"><%= job.get("nextFire") %><span class="rel"><%= job.get("nextFireRel") %></span></td>
|
||||
<td class="mono"><%= job.get("jobData") %></td>
|
||||
</tr>
|
||||
<tr class="trigger-row" id="triggers_<%= rowId %>">
|
||||
<td colspan="8" style="padding: 6px 10px 6px 30px;">
|
||||
<% if (tList.isEmpty()) { %>
|
||||
<em style="color:#a0aec0;">No triggers</em>
|
||||
<% } else { %>
|
||||
<table class="trigger-detail-table">
|
||||
<tr><th>Trigger Name</th><th>Group</th><th>Type</th><th>Expression / Interval</th><th>State</th><th>Priority</th><th>Prev Fire</th><th>Next Fire</th></tr>
|
||||
<% for (Map<String, Object> t : tList) { %>
|
||||
<tr>
|
||||
<td><%= t.get("triggerName") %></td>
|
||||
<td><%= t.get("triggerGroup") %></td>
|
||||
<td><%= t.get("type") %></td>
|
||||
<td class="mono"><%= t.get("cronExp") %></td>
|
||||
<td class="<%= t.get("stateClass") %>"><%= t.get("state") %></td>
|
||||
<td><%= t.get("priority") %></td>
|
||||
<td class="mono"><%= t.get("prevFire") %><span class="rel"><%= t.get("prevFireRel") %></span></td>
|
||||
<td class="mono"><%= t.get("nextFire") %><span class="rel"><%= t.get("nextFireRel") %></span></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% jobIdx++; } %>
|
||||
</table>
|
||||
<% } %>
|
||||
|
||||
<% if (clusteredScheduler != null) { %>
|
||||
<h2>Clustered Scheduler: <%= clusteredScheduler.getSchedulerName() %> (DB)</h2>
|
||||
<div class="scheduler-meta">
|
||||
<span><strong>Status:</strong> <span class="<%= clusteredScheduler.isStarted() ? "state-normal" : "state-error" %>"><%= clusteredScheduler.isStarted() ? "STARTED" : "STOPPED" %></span></span>
|
||||
<span><strong>Instance ID:</strong> <%= clusteredScheduler.getSchedulerInstanceId() %></span>
|
||||
<span><strong>Job Count:</strong> <%= getJobList(clusteredScheduler).size() %></span>
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Job Name</th>
|
||||
<th>Job Class</th>
|
||||
<th>Cron</th>
|
||||
<th>State</th>
|
||||
<th>Concurrent</th>
|
||||
<th>Prev Fire</th>
|
||||
<th>Next Fire</th>
|
||||
<th>Job Data</th>
|
||||
</tr>
|
||||
<% int cJobIdx = 0; for (Map<String, Object> job : getJobList(clusteredScheduler)) { String rowId = "c_" + cJobIdx; List<Map<String, Object>> tList = (List<Map<String, Object>>) job.get("triggerList"); %>
|
||||
<tr class="trigger-toggle" onclick="toggleTriggers('<%= rowId %>')">
|
||||
<td><span class="toggle-icon" id="icon_<%= rowId %>">▶</span><strong><%= job.get("name") %></strong><span class="trigger-count">(<%= job.get("triggerCount") %> triggers)</span></td>
|
||||
<td class="mono"><%= job.get("jobClass") %></td>
|
||||
<td class="mono"><%= job.get("cronExp") %></td>
|
||||
<td class="<%= job.get("stateClass") %>"><%= job.get("state") %></td>
|
||||
<td><span class="badge <%= (Boolean)job.get("disallowConcurrent") ? "badge-yes" : "badge-no" %>">
|
||||
<%= (Boolean)job.get("disallowConcurrent") ? "NO (protected)" : "YES (allowed)" %></span></td>
|
||||
<td class="mono"><%= job.get("prevFire") %><span class="rel"><%= job.get("prevFireRel") %></span></td>
|
||||
<td class="mono"><%= job.get("nextFire") %><span class="rel"><%= job.get("nextFireRel") %></span></td>
|
||||
<td class="mono"><%= job.get("jobData") %></td>
|
||||
</tr>
|
||||
<tr class="trigger-row" id="triggers_<%= rowId %>">
|
||||
<td colspan="8" style="padding: 6px 10px 6px 30px;">
|
||||
<% if (tList.isEmpty()) { %>
|
||||
<em style="color:#a0aec0;">No triggers</em>
|
||||
<% } else { %>
|
||||
<table class="trigger-detail-table">
|
||||
<tr><th>Trigger Name</th><th>Group</th><th>Type</th><th>Expression / Interval</th><th>State</th><th>Priority</th><th>Prev Fire</th><th>Next Fire</th></tr>
|
||||
<% for (Map<String, Object> t : tList) { %>
|
||||
<tr>
|
||||
<td><%= t.get("triggerName") %></td>
|
||||
<td><%= t.get("triggerGroup") %></td>
|
||||
<td><%= t.get("type") %></td>
|
||||
<td class="mono"><%= t.get("cronExp") %></td>
|
||||
<td class="<%= t.get("stateClass") %>"><%= t.get("state") %></td>
|
||||
<td><%= t.get("priority") %></td>
|
||||
<td class="mono"><%= t.get("prevFire") %><span class="rel"><%= t.get("prevFireRel") %></span></td>
|
||||
<td class="mono"><%= t.get("nextFire") %><span class="rel"><%= t.get("nextFireRel") %></span></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% cJobIdx++; } %>
|
||||
</table>
|
||||
<% } %>
|
||||
|
||||
<script>
|
||||
function toggleTriggers(rowId) {
|
||||
var row = document.getElementById('triggers_' + rowId);
|
||||
var icon = document.getElementById('icon_' + rowId);
|
||||
if (row.classList.contains('open')) {
|
||||
row.classList.remove('open');
|
||||
icon.classList.remove('open');
|
||||
} else {
|
||||
row.classList.add('open');
|
||||
icon.classList.add('open');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+15
-6
@@ -87,9 +87,8 @@ dependencies {
|
||||
//implementation project(':kjb-safedb')
|
||||
//implementation project(":eapim-admin-djb")
|
||||
|
||||
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
compileOnly files('libs/damo-manager.jar')
|
||||
/* Custom Libs */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
//implementation "org.dom4j:dom4j:2.1.3"
|
||||
//implementation "com.rabbitmq:amqp-client:3.6.6"
|
||||
@@ -114,9 +113,9 @@ dependencies {
|
||||
// https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
|
||||
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 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.2'
|
||||
|
||||
// apistatus-draft.yml (자동 탐지 초안 양식) 로딩용.
|
||||
// admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없어 직접 파싱한다.
|
||||
@@ -141,13 +140,18 @@ dependencies {
|
||||
|
||||
implementation "org.apache.ibatis:ibatis-sqlmap:2.3.4.726"
|
||||
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 "net.sf.json-lib:json-lib-ext-spring:1.0.2"
|
||||
implementation "taglibs:standard:1.1.2"
|
||||
implementation "javax.servlet:jstl:1.1.2"
|
||||
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}"
|
||||
|
||||
// https://mvnrepository.com/artifact/org.apache.poi/poi
|
||||
@@ -235,6 +239,11 @@ configurations.all {
|
||||
// 일부 transitive 가 끌어오는 xml-apis:1.0.b2 (2002, DOM L2) 는 이 클래스를 포함하지 않아
|
||||
// xercesImpl 가 NoClassDefFoundError 를 일으킴 → 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
|
||||
Content-Type: application/json
|
||||
User-Agent: insomnia/10.1.1
|
||||
X-API-KEY: EAPIM_KEY
|
||||
X-Internal-Token: {{internalApiToken}}
|
||||
action: insert
|
||||
target_host: stg
|
||||
|
||||
@@ -118,4 +118,4 @@ target_host: stg
|
||||
"errTransformYn": "N"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
- Oracle 19c
|
||||
- Spring Data JPA 2.5.2
|
||||
- Hibernate 5.4.33/5.6.15
|
||||
- QueryDSL 5.0.0
|
||||
- iBATIS 2.3.4 (레거시 SQL 매핑)
|
||||
- Quartz 2.2.1
|
||||
- Logback 1.2.10
|
||||
| 구분 | 버전 / 내용 |
|
||||
|---|---|
|
||||
| Java | **8** (Gradle toolchain 으로 강제, `JavaLanguageVersion.of(8)`) |
|
||||
| 빌드 | Gradle **8.7** (wrapper), `war` 패키징 |
|
||||
| Spring Framework | 5.3.27 (MVC / ORM / JDBC) |
|
||||
| Spring Data JPA | 2.5.2 |
|
||||
| Hibernate | 5.6.15.Final |
|
||||
| QueryDSL | 5.0.0 (빌드 시 Q-class 생성) |
|
||||
| iBATIS | 2.3.4 (레거시 SQL 매핑) |
|
||||
| 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 이상
|
||||
- Gradle 8.7
|
||||
- **JDK 8** - `.envrc`(direnv) 또는 셸 rc 에서 `JAVA_HOME` 을 JDK 8 위치로 export. Gradle 8 toolchain auto-detect 가 `JAVA_HOME` 도 후보로 인식합니다. `gradle.properties` 에 JDK 경로를 넣지 않습니다.
|
||||
- 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` - 개발 환경
|
||||
- `WebContent/WEB-INF/properties/env.T.properties` - 테스트/스테이징 환경
|
||||
- `WebContent/WEB-INF/properties/env.P.properties` - 운영 환경
|
||||
`settings.gradle` 에서 형제 디렉토리의 소스 프로젝트를 참조합니다.
|
||||
|
||||
| 모듈 | 위치 | 설명 |
|
||||
|---|---|---|
|
||||
| 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 엔티티) |
|
||||
|
||||
### 주의사항
|
||||
- **SafeDB**: 암호화 기능이 필요한 경우 `-Dkjb_safedb.mode=fake` 설정 (개발 환경) 또는 실제 SafeDB 라이브러리 설치
|
||||
- **멀티 모듈 구조**: eapim-online의 여러 모듈들을 참조하므로 반드시 eapim-online도 클론해야 합니다
|
||||
|
||||
> `kjb-safedb`, `eapim-admin-djb` 는 `settings.gradle` 에 주석 처리되어 있으며,
|
||||
> 필요 시 주석을 해제하고 해당 디렉토리를 체크아웃합니다.
|
||||
|
||||
## 빌드 및 실행
|
||||
|
||||
### Gradle 명령어
|
||||
|
||||
프로젝트 루트의 Gradle wrapper(`./gradlew`) 를 사용합니다.
|
||||
|
||||
### 빌드 및 실행 명령어
|
||||
|
||||
```bash
|
||||
# 표준 빌드
|
||||
./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 test
|
||||
# WAR 빌드 (build/libs/eapim-admin.war)
|
||||
./gradlew war
|
||||
|
||||
# 패키징 없이 클래스만 컴파일
|
||||
# WebLogic 배포용 빌드 (테스트 제외, weblogic-web.xml 사용)
|
||||
./gradlew build -x test -Pprofile=weblogic
|
||||
|
||||
# 패키징 없이 컴파일만
|
||||
./gradlew classes
|
||||
|
||||
# QueryDSL Q-classes 및 기타 애노테이션 생성
|
||||
# QueryDSL Q-class 등 생성 코드만 생성
|
||||
./gradlew compileJava
|
||||
|
||||
# 의존성 트리 보기
|
||||
./gradlew dependencies
|
||||
# 전체 테스트
|
||||
./gradlew test
|
||||
|
||||
# 사용 가능한 모든 태스크 목록
|
||||
# 특정 테스트
|
||||
./gradlew test --tests "com.eactive.eai.rms.*"
|
||||
|
||||
# 의존성 트리 / 태스크 목록
|
||||
./gradlew dependencies
|
||||
./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/
|
||||
├── agent/command - 커맨드 패턴 구현
|
||||
├── common/ - 공통 유틸리티, iBatis, JSON 직렬화
|
||||
├── custom/ - 고객사별 커스터마이제이션
|
||||
├── rms/ - 메인 애플리케이션 코드
|
||||
│ ├── bap/ - 배치 처리 (BAP)
|
||||
│ ├── bat/ - 추가 배치 기능
|
||||
│ ├── common/ - RMS 공통 (필터, 시작, 서비스)
|
||||
│ ├── data/ - 데이터 엔티티, 리포지토리
|
||||
│ ├── env/ - 환경 설정
|
||||
│ ├── kakao/ - 카카오 연동
|
||||
│ ├── onl/ - 온라인 트랜잭션 관리
|
||||
│ └── service/ - 비즈니스 서비스
|
||||
└── ext.kjb/ - 광주은행(KJB) 확장
|
||||
```
|
||||
├── common - 공통 유틸리티, iBatis, JSON 직렬화
|
||||
├── custom - 고객사별 커스터마이제이션
|
||||
├── rms - 메인 애플리케이션 코드
|
||||
│ ├── bap - 배치 처리(BAP): adaptor / manage / tansaction
|
||||
│ ├── bat - 추가 배치 기능
|
||||
│ ├── common - RMS 공통 (필터, 시작, 서비스)
|
||||
│ ├── data - 데이터 엔티티, 리포지토리
|
||||
│ ├── env - 환경 설정
|
||||
│ ├── kakao - 카카오 연동
|
||||
│ ├── onl - 온라인 트랜잭션 관리 (apim / manage / transaction)
|
||||
│ ├── service - 비즈니스 서비스
|
||||
│ └── ext
|
||||
│ ├── djb - 제주은행(DJB) 확장 (webhook, UMS 연동, 통계 등)
|
||||
│ └── kjb - 광주은행(KJB) 유래 확장
|
||||
└── (com.eactive.ext.kjb) - 통계 화면 등 일부 확장 코드
|
||||
|
||||
### 웹 리소스 구조
|
||||
```
|
||||
WebContent/
|
||||
├── WEB-INF/
|
||||
│ ├── applicationContext.xml - Root Spring 설정
|
||||
│ ├── springapp-servlet.xml - Servlet Spring 설정
|
||||
│ ├── web.xml - Tomcat용 배포 디스크립터
|
||||
│ ├── weblogic-web.xml - WebLogic용 배포 디스크립터
|
||||
│ └── properties/ - 환경별 프로퍼티
|
||||
│ ├── env.D.properties - 개발
|
||||
│ ├── env.T.properties - 테스트
|
||||
│ └── env.P.properties - 운영
|
||||
└── guide/ - 사용자 가이드
|
||||
│ ├── applicationContext.xml - Root Spring 설정
|
||||
│ ├── springapp-servlet.xml - Servlet Spring 설정
|
||||
│ ├── web.xml / weblogic-web.xml
|
||||
│ └── properties/ - env.D / env.T / env.P / env.L .properties
|
||||
├── jsp/ - 화면
|
||||
└── guide/ - 사용자 가이드(docsify)
|
||||
```
|
||||
|
||||
## 주요 기능
|
||||
## 데이터 액세스 (하이브리드 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 참조):
|
||||
|
||||
| 이름 | 버전 | 설명 |
|
||||
| ------------------------ | ------------ | ---------------------------------------------- |
|
||||
| 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을 통해 설정됩니다
|
||||
Hibernate Multi-tenancy 로 요청 시점에 스키마를 전환합니다.
|
||||
`DataSourceContextHolder`(ThreadLocal) → `TenantIdentifierResolver` → `ConfigurableMultiTenantConnectionProvider.setSchema()`.
|
||||
요청 파라미터 `serviceType`(예: `APIGW`) 를 `DataSourceTypeInterceptor` 가 읽어 자동 설정하거나, 컨트롤러에서 명시 설정합니다.
|
||||
자세한 내용은 `CLAUDE.md` 의 "Multi-tenancy" 절 참조.
|
||||
|
||||
## 코드 생성
|
||||
|
||||
빌드 프로세스에서 생성되는 것들:
|
||||
- **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)**: 개발 환경
|
||||
- **T (TEST)**: 테스트/스테이징 환경
|
||||
- **P (PROD)**: 운영 환경
|
||||
`-Deai.systemmode` 로 지정하며 해당 프로퍼티 파일을 로드합니다.
|
||||
|
||||
시스템 모드는 `-Deai.systemmode` JVM 옵션으로 설정하며, 해당 환경의 프로퍼티 파일(`env.D.properties` 등)을 로드합니다.
|
||||
| 값 | 환경 | 파일 |
|
||||
|---|---|---|
|
||||
| D | 개발 | `env.D.properties` |
|
||||
| T | 테스트 / 스테이징 | `env.T.properties` |
|
||||
| P | 운영 | `env.P.properties` |
|
||||
| L | 로컬 | `env.L.properties` |
|
||||
|
||||
### TOMCAT 기동 옵션 참조
|
||||
### 데이터베이스
|
||||
|
||||
#### 은행 개발 서버용
|
||||
```
|
||||
-Deai.datasource.type=DEV
|
||||
-Dinst.Name=emsSvr11
|
||||
-Deai.tableowner=EMSAPP
|
||||
-Deai.systemmode=D
|
||||
-Dfile.encoding=utf-8
|
||||
-DLOGBACK_LOG_LEVEL=info
|
||||
```
|
||||
- **Oracle**: 운영 주 DB (`hibernate.dialect=org.hibernate.dialect.Oracle12cDialect`)
|
||||
- **MariaDB / PostgreSQL**: 벤더별 SQL 매핑으로 지원 (JDBC 의존성은 필요 시 활성화)
|
||||
- **H2**: 테스트용 인메모리 DB (`src/test/resources`)
|
||||
- 연결 타입은 `-Deai.datasource.type`(DEV/STG/PROD) 로 선택
|
||||
|
||||
#### 로컬 개발용
|
||||
```
|
||||
-Deai.datasource.type=DEV
|
||||
-Dinst.Name=emsSvr99
|
||||
-Deai.tableowner=EMSAPP
|
||||
-Deai.systemmode=D
|
||||
-Dfile.encoding=utf-8
|
||||
-Dlogin.mode=db
|
||||
-DLOGBACK_LOG_LEVEL=DEBUG
|
||||
-Dlogback.configurationFile=classpath:logback-dev.xml
|
||||
-Dhibernate.dialect=org.hibernate.dialect.Oracle12cDialect
|
||||
-Dkjb_safedb.mode=fake
|
||||
### DB 암호화
|
||||
|
||||
`damo-manager.jar` 모듈은 환경에 따라 다른 암호화로직을 제공한다.
|
||||
|
||||
| 환경 | 설명 |
|
||||
|---|---|
|
||||
| 운영 | 실제 D'amo 네이티브 라이브러리 |
|
||||
| 개발 | SHA-256 |
|
||||
|
||||
로컬 개발 시 `-Ddamo-manager.enabled=false`
|
||||
|
||||
## 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 네이티브 라이브러리 사용
|
||||
- **FAKE**: SHA-256/AES-256을 사용하는 개발 모드 (SafeDB 설치 불필요)
|
||||
- **NONE**: 암호화 우회 (테스트 전용)
|
||||
**유량제어토큰실패감시**
|
||||
- `InflowTokenMonitorJob` - 유량제어 토큰 획득 실패(TSEAIFR11) 발생시 직원에게 메신저를 발송한다
|
||||
- `api.inflow.fail.range_minute` - 최근 몇분동안 감시하는지 설정
|
||||
|
||||
개발 시 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` 필요)
|
||||
- **JEUS** (지원)
|
||||
- **JBoss/WildFly** (descriptors와 함께 지원)
|
||||
| 대상 | 비고 |
|
||||
|---|---|
|
||||
| Tomcat | 기본, 개발 |
|
||||
| WebLogic 14.1.2 | 운영, `-Pprofile=weblogic` 필요 |
|
||||
| JEUS | 지원 |
|
||||
| JBoss / WildFly | descriptor 포함 지원 |
|
||||
|
||||
### WebLogic 전용 빌드
|
||||
|
||||
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
|
||||
|
||||
```bash
|
||||
gradle build -x test -Pprofile=weblogic
|
||||
```
|
||||
|
||||
### WAR 배포
|
||||
|
||||
WAR 파일은 context path `/monitoring`으로 `eapim-admin.war`로 빌드됩니다.
|
||||
- 모든 관리 변경사항은 DB 와 메모리에 즉시 반영됩니다.
|
||||
- 인코딩은 UTF-8. `CharacterEncodingFilter` 가 `*.excel` 을 제외한 모든 요청에 적용.
|
||||
- XSS 보호: `com.eactive.eai.rms.common.filter.CrossScriptingFilter` 전역 적용.
|
||||
|
||||
## IDE 설정
|
||||
|
||||
### Eclipse
|
||||
`build.gradle` 은 `eclipse` / `eclipse-wtp` / `idea` 플러그인을 모두 포함합니다.
|
||||
IntelliJ 전용 변형은 `build.gradle.intellij` 로 별도 관리합니다 (필요 시 `build.gradle` 로 교체).
|
||||
|
||||
```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)**: 테스트 프레임워크
|
||||
- **Spring Boot Test 2.6.15**: 통합 테스트 유틸리티
|
||||
- **H2 Database**: 테스트용 인메모리 데이터베이스
|
||||
- **Mockito**: 모킹 프레임워크 (Spring Boot Test를 통해)
|
||||
- **JUnit 5 (Jupiter)** - 테스트 프레임워크
|
||||
- **Spring Boot Test 2.6.15** - 통합 테스트 유틸리티
|
||||
- **H2** - 인메모리 DB (`src/test/resources`)
|
||||
- **Mockito** - 모킹 (Spring Boot Test 경유)
|
||||
|
||||
```bash
|
||||
# 모든 테스트 실행
|
||||
gradle test
|
||||
|
||||
# 특정 테스트 클래스 실행
|
||||
gradle test --tests "com.example.ClassName"
|
||||
|
||||
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
|
||||
gradle test --tests "com.eactive.eai.rms.*"
|
||||
./gradlew test
|
||||
./gradlew 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` (한글)
|
||||
- **사용자 가이드**: `WebContent/guide/` (한글)
|
||||
- **프로젝트 상세 지침**: `CLAUDE.md` (AI 어시스턴트 및 개발자용 마스터 문서, 한글)
|
||||
- **사용자 가이드**: `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.
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import com.eactive.eai.rms.common.acl.menu.ui.LeftMenuUI;
|
||||
import com.eactive.eai.rms.common.acl.role.RoleManService;
|
||||
import com.eactive.eai.rms.common.base.BaseAnnotationController;
|
||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||
import com.eactive.eai.rms.common.login.SessionManager;
|
||||
import com.eactive.eai.rms.data.entity.man.menu.MenuService;
|
||||
@@ -31,6 +32,9 @@ public class MenuRenderController extends BaseAnnotationController {
|
||||
@Autowired
|
||||
private MenuRenderService menuRenderService;
|
||||
|
||||
@Autowired
|
||||
private MonitoringContext monitoringContext;
|
||||
|
||||
@GetMapping("/left_top.do")
|
||||
public String left_top(HttpServletRequest request, ModelMap modelMap) {
|
||||
return "/common/screen/left_top";
|
||||
@@ -87,6 +91,9 @@ public class MenuRenderController extends BaseAnnotationController {
|
||||
List<DataSourceType> roleServiceTypeList = roleManService.selectRoleServiceTypeList(roleIds);
|
||||
modelMap.addAttribute("roleServiceTypeList", roleServiceTypeList);
|
||||
|
||||
modelMap.addAttribute("autoLogoutTimeout",
|
||||
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 30));
|
||||
|
||||
if (StringUtils.isNotBlank(mainPage)) {
|
||||
for (Map<String, Object> map : dataList) {
|
||||
String url = (String) map.get("LEV4_URL");
|
||||
|
||||
@@ -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.UserBusinessId;
|
||||
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;
|
||||
|
||||
@Service
|
||||
@@ -40,6 +43,9 @@ public class BizManService extends BaseService {
|
||||
@Autowired
|
||||
private UserBusinessService userBusinessService;
|
||||
|
||||
@Autowired
|
||||
private TransactionTrackingFilterService transactionTrackingFilterService;
|
||||
|
||||
@Autowired
|
||||
private UserBizUIMapper userBizUIMapper;
|
||||
|
||||
@@ -101,7 +107,14 @@ public class BizManService extends BaseService {
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
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()
|
||||
.map(e -> e.getId().getEaiBzwkDstcd())
|
||||
.collect(Collectors.joining(","));
|
||||
|
||||
@@ -258,7 +258,8 @@ public interface MonitoringContext {
|
||||
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 60)
|
||||
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
|
||||
|
||||
// 웹훅
|
||||
// 웹훅
|
||||
public static final String RMS_WEBHOOK_ENABLED = "rms.webhook.enabled";
|
||||
public static final String RMS_WEBHOOK_REVERSE_PROXY_URL = "rms.webhook.reverse_proxy.url";
|
||||
public static final String RMS_WEBHOOK_RETRY_COUNT = "rms.webhook.retry_count";
|
||||
public static final String RMS_WEBHOOK_RETRY_TIME = "rms.webhook.retry_time";
|
||||
@@ -268,6 +269,24 @@ public interface MonitoringContext {
|
||||
|
||||
// 최대 로그인 실패 허용횟수 (0이면 계정 잠금 미적용)
|
||||
public static final String RMS_PASSWORD_FAIL_COUNT = "rms.password.fail.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 상태변화 스윙챗 발송여부
|
||||
public static final String DJB_UMS_MESSENGER_APIMONITOR_ENABLED = "djb.ums.messenger.api-monitor.enabled";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -78,9 +78,16 @@ public class CrossScriptingFilter implements Filter {
|
||||
}
|
||||
|
||||
// Web Cache Deception
|
||||
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||
response.setHeader("Pragma", "no-cache");
|
||||
response.setDateHeader("Expires", 0);
|
||||
// js/css 정적 리소스는 캐시를 허용 (화면 오픈 시마다 재다운로드되는 문제 방지)
|
||||
String servletPath = request.getServletPath();
|
||||
boolean isStaticAsset = servletPath != null
|
||||
&& (servletPath.endsWith(".js") || servletPath.endsWith(".css"));
|
||||
|
||||
if (!isStaticAsset) {
|
||||
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||
response.setHeader("Pragma", "no-cache");
|
||||
response.setDateHeader("Expires", 0);
|
||||
}
|
||||
|
||||
chain.doFilter(new RequestWrapper(request), response);
|
||||
//
|
||||
|
||||
@@ -158,7 +158,7 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
||||
// src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과
|
||||
value = filterSrcAttribute(value);
|
||||
|
||||
// 리치 텍스트 파라미터는 문자 변환을 건너뛰고 XSS 패턴 필터링만 적용
|
||||
// 리치 텍스트 파라미터와 메시지 템플릿 저장 요청은 문자 변환 없이 XSS 패턴 필터링만 적용
|
||||
if (!shouldSkipCharConvert()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
value = convertChars(value, sb);
|
||||
@@ -179,6 +179,15 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
||||
if (param == null) {
|
||||
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) {
|
||||
if (StringUtils.equalsIgnoreCase(param, skipParam)) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.eactive.eai.rms.common.login;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChangePasswordResponseDto {
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 성공 여부
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 사용자에게 alert로 표시할 메시지
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 성공 시 이동할 URL
|
||||
*/
|
||||
private String redirectUrl;
|
||||
|
||||
/**
|
||||
* SMS 2차 인증 필요 여부 (true면 아직 비밀번호가 반영되지 않고 인증 대기 중)
|
||||
*/
|
||||
private boolean smsAuthRequired;
|
||||
|
||||
/**
|
||||
* 마스킹된 전화번호 (예: 010****1234)
|
||||
*/
|
||||
private String maskedPhone;
|
||||
|
||||
/**
|
||||
* 인증번호 만료 시각 (timestamp, 밀리초)
|
||||
*/
|
||||
private long expiresAt;
|
||||
|
||||
/**
|
||||
* 재발급 가능 시각 (timestamp, 밀리초)
|
||||
*/
|
||||
private long resendableAt;
|
||||
|
||||
/**
|
||||
* djb.sms_auth.mode=fixed 일 때만 채워지는 테스트용 인증번호 안내 문구 (예: "[테스트] 인증번호: 000000")
|
||||
*/
|
||||
private String smsTestHint;
|
||||
}
|
||||
@@ -50,4 +50,9 @@ public class LoginResponseDto {
|
||||
* 초기 비밀번호 변경 필요 여부
|
||||
*/
|
||||
private boolean changePassword;
|
||||
|
||||
/**
|
||||
* djb.sms_auth.mode=fixed 일 때만 채워지는 테스트용 인증번호 안내 문구 (예: "[테스트] 인증번호: 000000")
|
||||
*/
|
||||
private String smsTestHint;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import com.eactive.apim.portal.user.entity.UserInfo;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserLoginHistory;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserLoginHistoryService;
|
||||
import com.eactive.eai.rms.data.entity.man.user.AdminUserPasswordHistoryService;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserRole;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserRoleService;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
|
||||
@@ -62,10 +63,13 @@ public class MainController implements InterceptorSkipController {
|
||||
private static final String USER_NOT_FOUND_KEY_2 = "login.useridnotfound2";
|
||||
|
||||
private static final String REDIRECT_LOGIN_URL = "redirect:/emergency.jsp";
|
||||
private static final String REDIRECT_CHANGE_PASSWORD_URL = "redirect:/emergency.jsp";
|
||||
|
||||
private static final String USER_STATUS_LOCKED = "2";
|
||||
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_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 MonitoringContext monitoringContext;
|
||||
@@ -76,6 +80,7 @@ public class MainController implements InterceptorSkipController {
|
||||
private final LoginService loginService;
|
||||
private final MonitoringPropertyService monitoringPropertyService;
|
||||
private final UserLoginHistoryService userLoginHistoryService;
|
||||
private final AdminUserPasswordHistoryService userPasswordHistoryService;
|
||||
private final SmsAuthService smsAuthService;
|
||||
|
||||
@Autowired
|
||||
@@ -87,6 +92,7 @@ public class MainController implements InterceptorSkipController {
|
||||
UserServiceTypeService userServiceTypeService,
|
||||
MonitoringPropertyService monitoringPropertyService,
|
||||
UserLoginHistoryService userLoginHistoryService,
|
||||
AdminUserPasswordHistoryService userPasswordHistoryService,
|
||||
SmsAuthService smsAuthService) {
|
||||
this.localeMessage = localeMessage;
|
||||
this.monitoringContext = monitoringContext;
|
||||
@@ -96,6 +102,7 @@ public class MainController implements InterceptorSkipController {
|
||||
this.userServiceTypeService = userServiceTypeService;
|
||||
this.monitoringPropertyService = monitoringPropertyService;
|
||||
this.userLoginHistoryService = userLoginHistoryService;
|
||||
this.userPasswordHistoryService = userPasswordHistoryService;
|
||||
this.smsAuthService = smsAuthService;
|
||||
}
|
||||
|
||||
@@ -216,7 +223,7 @@ public class MainController implements InterceptorSkipController {
|
||||
// 휴대폰번호 없는 사용자
|
||||
return LoginResponseDto.builder()
|
||||
.success(false)
|
||||
.errorMessage("휴대폰번호가 등록되어 있지 않아 로그인할 수 없습니다. 관리자에게 문의하세요.")
|
||||
.errorMessage("NO_PHONE_NUMBER")
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -227,7 +234,7 @@ public class MainController implements InterceptorSkipController {
|
||||
if (!sent) {
|
||||
return LoginResponseDto.builder()
|
||||
.success(false)
|
||||
.errorMessage("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.")
|
||||
.errorMessage("FAIL_SEND_AUTH")
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -246,6 +253,7 @@ public class MainController implements InterceptorSkipController {
|
||||
.maskedPhone(smsAuthService.maskPhoneNumber(userInfo.getCphnno()))
|
||||
.expiresAt(smsAuthService.getExpiresAt(session))
|
||||
.resendableAt(smsAuthService.getResendableAt(session))
|
||||
.smsTestHint(smsAuthService.getFixedModeHintText())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -453,6 +461,35 @@ public class MainController implements InterceptorSkipController {
|
||||
MonitoringContext.RMS_PASSWORD_FAIL_COUNT, DEFAULT_MAX_LOGIN_FAIL_COUNT);
|
||||
}
|
||||
|
||||
// 비밀번호 재사용 금지 대상 최근 이력 개수 조회 (rms.password.history.count)
|
||||
private int getPasswordHistoryCount() {
|
||||
return monitoringContext.getIntProperty(
|
||||
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) {
|
||||
int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1;
|
||||
@@ -684,7 +721,7 @@ public class MainController implements InterceptorSkipController {
|
||||
model.addAttribute("mainPage", mainPage);
|
||||
model.addAttribute("menuId", menuId);
|
||||
model.addAttribute("autoLogoutTimeout",
|
||||
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 60));
|
||||
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 30));
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("CURRENT SERVICE TYPE : " + service);
|
||||
@@ -824,7 +861,8 @@ public class MainController implements InterceptorSkipController {
|
||||
}
|
||||
|
||||
@RequestMapping("/changePassword.do")
|
||||
public String changePassword(HttpServletRequest request,
|
||||
@ResponseBody
|
||||
public ChangePasswordResponseDto changePassword(HttpServletRequest request,
|
||||
String changePassword, String confirmPassword,
|
||||
String resetUserId, String resetPassword) {
|
||||
HttpSession session = request.getSession();
|
||||
@@ -842,9 +880,10 @@ public class MainController implements InterceptorSkipController {
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
|
||||
localeMessage
|
||||
.getString(USER_NOT_FOUND_KEY_1));
|
||||
session.setAttribute(RESULT_MSG, localeMessage
|
||||
.getString(USER_NOT_FOUND_KEY_1));
|
||||
return REDIRECT_LOGIN_URL;
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage(localeMessage.getString(USER_NOT_FOUND_KEY_1)))
|
||||
.build();
|
||||
}
|
||||
|
||||
UserInfo userInfo = userInfoOptional.get();
|
||||
@@ -854,9 +893,10 @@ public class MainController implements InterceptorSkipController {
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
|
||||
localeMessage
|
||||
.getString("login.pwdmismatch1"));
|
||||
session.setAttribute(RESULT_MSG, localeMessage
|
||||
.getString("login.pwdmismatch2"));
|
||||
return REDIRECT_CHANGE_PASSWORD_URL;
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage(localeMessage.getString("login.pwdmismatch2")))
|
||||
.build();
|
||||
}
|
||||
|
||||
// 2. 비밀번호 Validation
|
||||
@@ -864,48 +904,154 @@ public class MainController implements InterceptorSkipController {
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
|
||||
localeMessage
|
||||
.getString("login.pwdmismatch3"));
|
||||
session.setAttribute(RESULT_MSG, localeMessage
|
||||
.getString("login.pwdmismatch4"));
|
||||
return REDIRECT_CHANGE_PASSWORD_URL;
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage(localeMessage.getString("login.pwdmismatch4")))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
// 3. 비밀번호 체크
|
||||
if (!userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256,resetPassword))
|
||||
|| !userInfo.getUserid().equals(resetUserId)) {
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
|
||||
localeMessage
|
||||
.getString("login.pwdmismatch5"));
|
||||
session.setAttribute(RESULT_MSG, localeMessage
|
||||
.getString("login.pwdmismatch6"));
|
||||
return REDIRECT_CHANGE_PASSWORD_URL;
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage(localeMessage.getString("login.pwdmismatch6")))
|
||||
.build();
|
||||
}
|
||||
|
||||
// 4. 비밀번호 변경
|
||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256,changePassword));
|
||||
userInfoService.save(userInfo);
|
||||
// 4. 최근 사용한 비밀번호 재사용 금지
|
||||
String newPasswordHash = DamoManager.getInstance().hash(DamoManager.SHA256, changePassword);
|
||||
if (userPasswordHistoryService.isPasswordReused(resetUserId, newPasswordHash, getPasswordHistoryCount())) {
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
|
||||
localeMessage
|
||||
.getString("login.pwdreused1"));
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage(localeMessage.getString("login.pwdreused2")))
|
||||
.build();
|
||||
}
|
||||
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "S", localeMessage
|
||||
.getString("login.pwdchanged"));
|
||||
session.setAttribute(RESULT_MSG, localeMessage
|
||||
.getString("login.pwdchanged"));
|
||||
logger.debug("\n\n\n\n===================" + localeMessage
|
||||
.getString("login.pwdchanged"));
|
||||
return REDIRECT_LOGIN_URL;
|
||||
// 5. SMS 2차 인증 필요 여부 체크 (비상모드는 로그인과 동일하게 우회)
|
||||
// djb.sms_auth.enabled 와 rms.password.sms_auth.enabled 가 모두 true 일 때만 SMS 인증 수행
|
||||
Boolean emergencyMode = (Boolean) session.getAttribute("emergencyMode");
|
||||
if (!Boolean.TRUE.equals(emergencyMode) && smsAuthService.isEnabled() && isPasswordChangeSmsAuthEnabled()) {
|
||||
if (!smsAuthService.hasValidPhoneNumber(userInfo)) {
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F", "휴대폰번호 미등록으로 비밀번호 변경 불가");
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage("휴대폰번호가 등록되어 있지 않아 비밀번호를 변경할 수 없습니다. 관리자에게 문의하세요."))
|
||||
.build();
|
||||
}
|
||||
|
||||
String authCode = smsAuthService.generateAuthCode();
|
||||
boolean sent = smsAuthService.sendAuthCode(userInfo, authCode);
|
||||
if (!sent) {
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요."))
|
||||
.build();
|
||||
}
|
||||
|
||||
// 실제 저장은 SMS 인증 성공 후(commitPendingPasswordChange)에 수행한다.
|
||||
smsAuthService.setupPasswordChangeAuthSession(session, userInfo, newPasswordHash, authCode);
|
||||
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(true)
|
||||
.smsAuthRequired(true)
|
||||
.maskedPhone(smsAuthService.maskPhoneNumber(userInfo.getCphnno()))
|
||||
.expiresAt(smsAuthService.getExpiresAt(session))
|
||||
.resendableAt(smsAuthService.getResendableAt(session))
|
||||
.smsTestHint(smsAuthService.getFixedModeHintText())
|
||||
.build();
|
||||
}
|
||||
|
||||
// SMS 인증 비활성화 또는 비상모드 - 기존처럼 즉시 반영
|
||||
return applyPasswordChange(request, userInfo, newPasswordHash);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("changePassword Error ", e);
|
||||
return "redirect:/common/errors/errorLogon.jsp";
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage(localeMessage.getString("login.pwdchangefail")))
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 SMS 인증 성공 후 호출. 세션에 대기 중인 새 비밀번호를 실제로 반영한다.
|
||||
* ({@code SmsAuthController}에서 인증번호 검증 성공 시 호출)
|
||||
*/
|
||||
public ChangePasswordResponseDto commitPendingPasswordChange(HttpServletRequest request, HttpSession session) {
|
||||
UserInfo userInfo = smsAuthService.getUserInfoFromSession(session);
|
||||
String pendingPasswordHash = smsAuthService.getPendingPasswordHashFromSession(session);
|
||||
|
||||
if (userInfo == null || pendingPasswordHash == null) {
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage("인증 세션이 만료되었습니다. 다시 시도해주세요."))
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
ChangePasswordResponseDto result = applyPasswordChange(request, userInfo, pendingPasswordHash);
|
||||
smsAuthService.clearAuthSession(session);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
logger.error("commitPendingPasswordChange Error ", e);
|
||||
smsAuthService.clearAuthSession(session);
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message(toAlertMessage(localeMessage.getString("login.pwdchangefail")))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 저장 + 변경 이력 기록 (SMS 인증 완료 여부와 무관하게 최종 반영 단계에서 공통 호출)
|
||||
private ChangePasswordResponseDto applyPasswordChange(HttpServletRequest request, UserInfo userInfo, String newPasswordHash) {
|
||||
userInfo.setPassword(newPasswordHash);
|
||||
userInfoService.save(userInfo);
|
||||
|
||||
userPasswordHistoryService.recordPasswordChange(userInfo.getUserid(), newPasswordHash);
|
||||
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "S", localeMessage
|
||||
.getString("login.pwdchanged"));
|
||||
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(true)
|
||||
.message(toAlertMessage(localeMessage.getString("login.pwdchanged")))
|
||||
.redirectUrl(request.getContextPath() + "/emergency.jsp")
|
||||
.build();
|
||||
}
|
||||
|
||||
private static final int PASSWORD_PATTERN_CHECK_LENGTH = 3;
|
||||
|
||||
private static final String[] KEYBOARD_SEQUENTIAL_ROWS = {
|
||||
"1234567890",
|
||||
"!@#$%^&*()",
|
||||
"qwertyuiop",
|
||||
"asdfghjkl",
|
||||
"zxcvbnm"
|
||||
};
|
||||
|
||||
private boolean isPasswordValid(String password) {
|
||||
|
||||
return password != null && password.getBytes().length == password
|
||||
.length() && password.getBytes().length >= 7
|
||||
&& isCombination(password)
|
||||
&& !isRepeatPassword(password, 5)
|
||||
&& !isSerialPassword(password, 5);
|
||||
if (password == null || password.getBytes().length != password.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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)
|
||||
&& !isKeyboardSequentialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH);
|
||||
}
|
||||
|
||||
private boolean isRepeatPassword(String password, int count) {
|
||||
@@ -930,27 +1076,47 @@ public class MainController implements InterceptorSkipController {
|
||||
}
|
||||
|
||||
private boolean isSerialPassword(String password, int count) {
|
||||
int preData = 0;
|
||||
int serialcount = 1;
|
||||
int ascCount = 1;
|
||||
int descCount = 1;
|
||||
|
||||
for (int i = 0; i < password.length(); i++) {
|
||||
for (int i = 1; i < password.length(); i++) {
|
||||
|
||||
if (preData + 1 == password.charAt(i)) {
|
||||
serialcount++;
|
||||
} else {
|
||||
serialcount = 1;
|
||||
}
|
||||
char prev = password.charAt(i - 1);
|
||||
char curr = password.charAt(i);
|
||||
|
||||
preData = password.charAt(i);
|
||||
ascCount = (curr == prev + 1) ? ascCount + 1 : 1;
|
||||
descCount = (curr == prev - 1) ? descCount + 1 : 1;
|
||||
|
||||
if (serialcount == count) { return true; }
|
||||
if (ascCount >= count || descCount >= count) { return true; }
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCombination(String password) {
|
||||
// 키보드 자판 배열상 연속된 문자열(qwer, asdf 등, 역순 포함) 사용 여부 체크
|
||||
private boolean isKeyboardSequentialPassword(String password, int count) {
|
||||
String lowerPassword = password.toLowerCase();
|
||||
|
||||
for (String row : KEYBOARD_SEQUENTIAL_ROWS) {
|
||||
|
||||
for (int i = 0; i <= row.length() - count; i++) {
|
||||
|
||||
String forward = row.substring(i, i + count);
|
||||
String backward = new StringBuilder(forward).reverse().toString();
|
||||
|
||||
if (lowerPassword.contains(forward) || lowerPassword.contains(backward)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCombination(String password, int minTypes) {
|
||||
int combicount = 0;
|
||||
// 숫자포함여부
|
||||
String pattern = "[0-9]+";
|
||||
@@ -979,8 +1145,8 @@ public class MainController implements InterceptorSkipController {
|
||||
combicount++;
|
||||
}
|
||||
|
||||
// combicount가 2 이상이면 true, 그렇지 않으면 false 반환
|
||||
return combicount >= 2;
|
||||
// combicount가 minTypes 이상이면 true, 그렇지 않으면 false 반환
|
||||
return combicount >= minTypes;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,23 +19,7 @@ public class ClusteredSchedulerConfig {
|
||||
|
||||
@Autowired @Qualifier("MONITORING")
|
||||
private DataSourceType monitoringDatasource;
|
||||
|
||||
/*
|
||||
public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory {
|
||||
@Override
|
||||
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
|
||||
Object job = super.createJobInstance(bundle);
|
||||
applicationContext.getAutowireCapableBeanFactory().autowireBean(job);
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SpringBeanJobFactory springBeanJobFactory() {
|
||||
return new AutowiringSpringBeanJobFactory();
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
@Bean(name = "clusteredScheduler")
|
||||
public SchedulerFactoryBean clusteredScheduler() {
|
||||
|
||||
@@ -55,7 +39,7 @@ public class ClusteredSchedulerConfig {
|
||||
quartzProperties.put("org.quartz.threadPool.threadCount", "5");
|
||||
quartzProperties.put("org.quartz.threadPool.threadPriority", "5");
|
||||
|
||||
quartzProperties.put("org.quartz.jobStore.misfireThreshold", "60000");
|
||||
quartzProperties.put("org.quartz.jobStore.misfireThreshold", "10000");
|
||||
quartzProperties.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
|
||||
quartzProperties.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
|
||||
quartzProperties.put("org.quartz.jobStore.useProperties", "false");
|
||||
@@ -81,11 +65,15 @@ public class ClusteredSchedulerConfig {
|
||||
factory.setQuartzProperties(quartzProperties);
|
||||
factory.setAutoStartup(true);
|
||||
factory.setSchedulerName("EMS_ClusteredScheduler");
|
||||
// factory.setJobFactory(springBeanJobFactory());
|
||||
|
||||
|
||||
AutoWiringSpringBeanJobFactory jobFactory =
|
||||
new AutoWiringSpringBeanJobFactory(applicationContext.getAutowireCapableBeanFactory());
|
||||
jobFactory.setApplicationContext(applicationContext);
|
||||
factory.setJobFactory(jobFactory);
|
||||
|
||||
factory.setApplicationContextSchedulerContextKey("applicationContext");
|
||||
factory.setApplicationContext(applicationContext);
|
||||
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.eactive.eai.rms.common.vo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
@@ -10,7 +11,9 @@ public class GridResponse<T> {
|
||||
private Integer page;
|
||||
private Long records;
|
||||
private List<T> rows;
|
||||
|
||||
/** jqGrid footerrow + userDataOnFooter 용 하단 합계행 데이터 (colModel name -> value) */
|
||||
private Map<String, Object> userdata;
|
||||
|
||||
public GridResponse() {
|
||||
}
|
||||
|
||||
@@ -53,7 +56,15 @@ public class GridResponse<T> {
|
||||
public void setRows(List<T> rows) {
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> getUserdata() {
|
||||
return userdata;
|
||||
}
|
||||
|
||||
public void setUserdata(Map<String, Object> userdata) {
|
||||
this.userdata = userdata;
|
||||
}
|
||||
|
||||
public GridResponse(List<T> rows) {
|
||||
this.rows = rows;
|
||||
this.total = 1;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.eactive.eai.rms.data.entity.man.user;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import org.hibernate.annotations.Comment;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import com.eactive.eai.data.entity.AbstractEntity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
|
||||
/**
|
||||
* 관리콘솔(TSEAIRM02) 로그인 사용자의 비밀번호 변경 이력 (TSEAIRM08).
|
||||
* 포털 API 소비자 비밀번호 이력({@code com.eactive.apim.portal.portaluser.entity.UserPasswordHistory})과는 별개 테이블/도메인이다.
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
|
||||
|
||||
@Entity
|
||||
@Table(name = "TSEAIRM08")
|
||||
@org.hibernate.annotations.Table(appliesTo = "TSEAIRM08", comment = "관리콘솔 사용자 비밀번호 변경 이력")
|
||||
public class AdminUserPasswordHistory extends AbstractEntity<Integer> implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GenericGenerator(
|
||||
name = "TSEAIRM08_GEN",
|
||||
strategy = "com.eactive.eai.rms.data.jpa.SchemaPrefixedSequenceGenerator",
|
||||
parameters = {
|
||||
@org.hibernate.annotations.Parameter(name = "sequence_name", value = "SEQ_TSEAIRM08"),
|
||||
@org.hibernate.annotations.Parameter(name = "initial_value", value = "1"),
|
||||
@org.hibernate.annotations.Parameter(name = "increment_size", value = "1")
|
||||
}
|
||||
)
|
||||
@GeneratedValue(
|
||||
strategy = GenerationType.SEQUENCE,
|
||||
generator = "TSEAIRM08_GEN"
|
||||
)
|
||||
@Column(name = "ID", unique = true, nullable = false)
|
||||
@Comment("비밀번호 변경 이력 식별 ID")
|
||||
private int pwdHistId;
|
||||
|
||||
@Column(name = "USERID", length = 255, nullable = false)
|
||||
@Comment("사용자 식별자(사번)")
|
||||
private String userId;
|
||||
|
||||
@Column(name = "PASSWORD_HASH", length = 255, nullable = false)
|
||||
@Comment("변경된 비밀번호 해시값")
|
||||
private String passwordHash;
|
||||
|
||||
@Column(name = "CHANGE_DATE", length = 14)
|
||||
@Comment("비밀번호 변경 일시 (yyyyMMddHHmmss)")
|
||||
private String changeDate;
|
||||
|
||||
@Column(name = "CREATED_BY", length = 200)
|
||||
@Comment("등록자 식별자")
|
||||
private String createdBy;
|
||||
|
||||
@Column(name = "CREATED_DATE", length = 14)
|
||||
@Comment("레코드 생성 일시 (yyyyMMddHHmmss)")
|
||||
private String createdDate;
|
||||
|
||||
@Override
|
||||
public @NonNull Integer getId() {
|
||||
return pwdHistId;
|
||||
}
|
||||
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.eactive.eai.rms.data.entity.man.user;
|
||||
|
||||
import com.eactive.eai.rms.data.EMSDataSource;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
|
||||
@EMSDataSource
|
||||
public interface AdminUserPasswordHistoryRepository extends BaseRepository<AdminUserPasswordHistory, Integer> {
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.eactive.eai.rms.data.entity.man.user;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
|
||||
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
|
||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
|
||||
@Service
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
public class AdminUserPasswordHistoryService
|
||||
extends AbstractEMSDataSerivce<AdminUserPasswordHistory, Integer, AdminUserPasswordHistoryRepository> {
|
||||
|
||||
/** 사용자의 최근 비밀번호 이력을 최신순으로 historyCount건 조회 */
|
||||
public List<AdminUserPasswordHistory> findRecent(String userId, int historyCount) {
|
||||
BooleanExpression predicate = QAdminUserPasswordHistory.adminUserPasswordHistory.userId.eq(userId);
|
||||
Pageable pageable = PageRequest.of(0, historyCount, Sort.by(Sort.Direction.DESC, "pwdHistId"));
|
||||
|
||||
return repository.findAll(predicate, pageable).getContent();
|
||||
}
|
||||
|
||||
/** 최근 historyCount건 이내에 동일한 비밀번호(해시값)가 있었는지 확인 */
|
||||
public boolean isPasswordReused(String userId, String newPasswordHash, int historyCount) {
|
||||
if (historyCount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return findRecent(userId, historyCount).stream()
|
||||
.anyMatch(history -> history.getPasswordHash().equals(newPasswordHash));
|
||||
}
|
||||
|
||||
/** 비밀번호 변경 이력 저장 */
|
||||
public void recordPasswordChange(String userId, String passwordHash) {
|
||||
String now = LocalDateTime.now().format(LocalDateTimeFormatters.FORMATTER_YYYYMMDDHHMMSS_14);
|
||||
|
||||
AdminUserPasswordHistory history = new AdminUserPasswordHistory();
|
||||
history.setUserId(userId);
|
||||
history.setPasswordHash(passwordHash);
|
||||
history.setChangeDate(now);
|
||||
history.setCreatedBy(userId);
|
||||
history.setCreatedDate(now);
|
||||
|
||||
save(history);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.rms.onl.apim.apigroup.ui.ApiGroupUISearch;
|
||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
import com.querydsl.core.Tuple;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
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) {
|
||||
List<ApiGroup> apiGroups = repository.findAll();
|
||||
for (ApiGroup apiGroup : apiGroups) {
|
||||
@@ -77,6 +98,28 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
|
||||
.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 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */
|
||||
public void setApiGroupsForApi(String apiId, List<String> groupIds) {
|
||||
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();
|
||||
}
|
||||
@@ -30,4 +30,12 @@ public class WebhookReq {
|
||||
@Column(name = "SECRET", length = 200)
|
||||
@Comment("HMAC 서명용 Secret Key")
|
||||
private String secret;
|
||||
|
||||
@Column(name = "USER_SECRET", length = 200)
|
||||
@Comment("사용자 Secret Key")
|
||||
private String userSecret;
|
||||
|
||||
@Column(name = "CREATED_DATE", length = 14)
|
||||
@Comment("신청일시 (yyyyMMddHHmmss)")
|
||||
private String createdDate;
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,6 +20,6 @@ public class WebhookReqEventId implements Serializable {
|
||||
private String webhookReqId;
|
||||
|
||||
@Column(name = "EVENT_TYPE", nullable = false, length = 50)
|
||||
@Comment("이벤트 유형 (CONTROL_START, ERROR_START 등)")
|
||||
@Comment("이벤트 유형 (CHECK_START, ERROR_START 등)")
|
||||
private String eventType;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ public class WebhookSendLog {
|
||||
|
||||
@Column(name = "SEND_BY", length = 100)
|
||||
private String sendBy;
|
||||
|
||||
@Column(name = "USER_SECRET")
|
||||
private String userSecret;
|
||||
|
||||
/* Boolean 편의 메서드 */
|
||||
public void setSuccess(Boolean success) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.eaimsg;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -13,7 +14,11 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
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.apim.apigroup.QApiGroup;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
|
||||
import com.eactive.eai.data.entity.onl.authserver.QScopeEntity;
|
||||
import com.eactive.eai.data.entity.onl.logger.QStatLog;
|
||||
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
||||
@@ -31,6 +36,9 @@ import com.querydsl.core.types.OrderSpecifier;
|
||||
import com.querydsl.core.types.Projections;
|
||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
import com.querydsl.core.types.dsl.Expressions;
|
||||
import com.querydsl.core.types.dsl.StringPath;
|
||||
import com.querydsl.jpa.JPAExpressions;
|
||||
import com.querydsl.jpa.JPQLQuery;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
|
||||
@Service
|
||||
@@ -38,8 +46,15 @@ import com.querydsl.jpa.impl.JPAQuery;
|
||||
@RequiredArgsConstructor
|
||||
public class EAIMessageService extends AbstractDataService<EAIMessageEntity, String, EAIMessageRepository> {
|
||||
|
||||
/** Oracle IN 절 표현식 개수 상한. 넘기면 ORA-01795 라 나눠 OR 로 잇는다 */
|
||||
private static final int ORACLE_IN_LIMIT = 1000;
|
||||
|
||||
private final UserBusinessService userBusinessService;
|
||||
|
||||
/** EMSAPP(PTL_API_SPEC_INFO) 조회용. 다른 스키마라 EAI 메시지 쿼리에 조인할 수 없다 */
|
||||
private final ApiSpecInfoRepository apiSpecInfoRepository;
|
||||
private final ApiSpecStatusRepository apiSpecStatusRepository;
|
||||
|
||||
public Page<Tuple> selectList(EAIMessageUISearch eaiMessageUISearch, Pageable pageable, String sortname, String sortorder) {
|
||||
|
||||
QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity;
|
||||
@@ -115,6 +130,7 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
||||
return getJPAQueryFactory()
|
||||
.select(qeaiMessageEntity
|
||||
, qServiceMessageEntity.psvintfacdsticname
|
||||
, qServiceMessageEntity.psvsysadptrbzwkgroupname
|
||||
, qRouting.nonmotivrouturiname
|
||||
, qStdInfo.apifullpath
|
||||
, qStdInfo.bzwksvckeyname
|
||||
@@ -136,8 +152,8 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
||||
|
||||
QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity;
|
||||
QServiceMessageEntity qServiceMessageEntity = QServiceMessageEntity.serviceMessageEntity;
|
||||
QRouting qRouting = QRouting.routing;
|
||||
QStandardMessageInfo qStdInfo = QStandardMessageInfo.standardMessageInfo;
|
||||
QApiStatus qApiStatus = QApiStatus.apiStatus;
|
||||
|
||||
BooleanBuilder predicate = new BooleanBuilder();
|
||||
|
||||
@@ -154,11 +170,10 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchEaiSvcDesc())) {
|
||||
predicate.and(qeaiMessageEntity.eaisvcdesc.containsIgnoreCase(eaiMessageUISearch.getSearchEaiSvcDesc()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchSvcMotivUsedStcd())) {
|
||||
predicate.and(qeaiMessageEntity.svcmotivusedstcd.eq(eaiMessageUISearch.getSearchSvcMotivUsedStcd()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchFlowControl())) {
|
||||
predicate.and(qeaiMessageEntity.flowctrlroutname.eq(eaiMessageUISearch.getSearchFlowControl()));
|
||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchGstatSysAdptrBzwkGroupName())) {
|
||||
predicate
|
||||
.and(qeaiMessageEntity.gstatsysadptrbzwkgroupname
|
||||
.containsIgnoreCase(eaiMessageUISearch.getSearchGstatSysAdptrBzwkGroupName()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchTranType())) {
|
||||
predicate.and(qeaiMessageEntity.trantype.eq(eaiMessageUISearch.getSearchTranType()));
|
||||
@@ -178,28 +193,114 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
||||
predicate.and(qeaiMessageEntity.refkey.containsIgnoreCase(eaiMessageUISearch.getSearchRefKey()));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchQueueName())) {
|
||||
predicate
|
||||
.and(qeaiMessageEntity.flowctrlroutname.eq(qRouting.routname))
|
||||
.and(qRouting.nonmotivrouturiname.containsIgnoreCase(eaiMessageUISearch.getSearchQueueName()));
|
||||
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchPsvintfacdsticName())) {
|
||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchPsvSysAdptrBzwkGroupName())) {
|
||||
predicate
|
||||
.and(qServiceMessageEntity.id.svcprcssno.eq(1))
|
||||
.and(qServiceMessageEntity.psvintfacdsticname
|
||||
.containsIgnoreCase(eaiMessageUISearch.getSearchPsvintfacdsticName()));
|
||||
.and(qServiceMessageEntity.psvsysadptrbzwkgroupname
|
||||
.containsIgnoreCase(eaiMessageUISearch.getSearchPsvSysAdptrBzwkGroupName()));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(eaiMessageUISearch.getSearchApiFullPath())) {
|
||||
predicate
|
||||
.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())) {
|
||||
String statusCode = eaiMessageUISearch.getSearchStatusCode();
|
||||
if ("N".equals(statusCode)) {
|
||||
// 정상 : API_STATUS 에 코드가 없거나(join 안 됨) E/D/C 가 아닌 경우
|
||||
predicate.and(qApiStatus.statusCode.isNull().or(qApiStatus.statusCode.notIn("E", "D", "C")));
|
||||
} else {
|
||||
predicate.and(qApiStatus.statusCode.eq(statusCode));
|
||||
}
|
||||
}
|
||||
|
||||
if ("Y".equals(eaiMessageUISearch.getSearchApiSpecYn())) {
|
||||
predicate.and(apiSpecRegisteredExpression(qeaiMessageEntity.eaisvcname));
|
||||
}
|
||||
|
||||
if ("Y".equals(eaiMessageUISearch.getSearchPortalDisplayYn())) {
|
||||
predicate.and(qeaiMessageEntity.eaisvcname.in(displayedGroupApiIdSubQuery()));
|
||||
}
|
||||
|
||||
return predicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAPI Spec 등록 여부 조건.
|
||||
*
|
||||
* <p>PTL_API_SPEC_INFO 는 EMSAPP 스키마라 TSEAIHE01 쿼리에 조인하면 ORA-00942 다. 스펙이 있는
|
||||
* API ID 를 EMS 커넥션에서 따로 읽어 IN 절로 넘긴다.</p>
|
||||
*/
|
||||
private BooleanExpression apiSpecRegisteredExpression(StringPath eaiSvcNamePath) {
|
||||
return apiIdInExpression(eaiSvcNamePath, apiSpecInfoRepository.findAllApiIds());
|
||||
}
|
||||
|
||||
private BooleanExpression apiIdInExpression(StringPath eaiSvcNamePath, List<String> specApiIds) {
|
||||
if (specApiIds.isEmpty()) {
|
||||
// in(빈 리스트) 는 JPQL 이 깨지므로 항상 거짓인 조건으로 대체한다
|
||||
return Expressions.ONE.eq(Expressions.TWO);
|
||||
}
|
||||
|
||||
BooleanExpression expression = null;
|
||||
for (int from = 0; from < specApiIds.size(); from += ORACLE_IN_LIMIT) {
|
||||
int to = Math.min(from + ORACLE_IN_LIMIT, specApiIds.size());
|
||||
BooleanExpression chunk = eaiSvcNamePath.in(specApiIds.subList(from, to));
|
||||
expression = (expression == null) ? chunk : expression.or(chunk);
|
||||
}
|
||||
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 메시지와 같은 스키마다 */
|
||||
private JPQLQuery<String> displayedGroupApiIdSubQuery() {
|
||||
QApiGroupApi qApiGroupApi = QApiGroupApi.apiGroupApi;
|
||||
QApiGroup qApiGroup = QApiGroup.apiGroup;
|
||||
|
||||
return JPAExpressions
|
||||
.select(qApiGroupApi.id.apiId)
|
||||
.from(qApiGroupApi)
|
||||
.join(qApiGroupApi.apiGroup, qApiGroup)
|
||||
.where(qApiGroup.displayYn.eq("1"));
|
||||
}
|
||||
|
||||
public Page<RoutingEaiMsgDto> findAll(Pageable pageable, RoutingEaiMsgSearch routingEaiMsgSearch) {
|
||||
|
||||
List<RoutingEaiMsgDto> result = routingEaiMsgDtoList(pageable, routingEaiMsgSearch);
|
||||
|
||||
@@ -8,18 +8,31 @@ public class EAIMessageUISearch {
|
||||
private String searchEaiSvcName;
|
||||
private String searchEaiBzwkDstcd;
|
||||
private String searchEaiSvcDesc;
|
||||
private String searchSvcMotivUsedStcd;
|
||||
private String searchFlowControl;
|
||||
private String searchGstatSysAdptrBzwkGroupName;
|
||||
private String searchTranType;
|
||||
private String searchQueueName;
|
||||
|
||||
private String rmsMenuAuth;
|
||||
private String searchPsvintfacdsticName;
|
||||
private String searchPsvSysAdptrBzwkGroupName;
|
||||
private String searchExtBizCd;
|
||||
|
||||
private String tranChangeSim;
|
||||
private String searchRefKey;
|
||||
|
||||
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" 이면 정상(코드 없음)만 조회 */
|
||||
private String searchStatusCode;
|
||||
|
||||
/** "Y" 면 PTL_API_SPEC_INFO 에 OpenAPI Spec 이 등록된 API 만 조회 */
|
||||
private String searchApiSpecYn;
|
||||
|
||||
/** "Y" 면 노출 중인(display_yn='1') API 그룹에 편성된 API 만 조회 */
|
||||
private String searchPortalDisplayYn;
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -62,6 +62,8 @@ public class InboundErrorInfoService extends AbstractDataService<InboundErrorInf
|
||||
List<InboundErrorInfo> list = query
|
||||
.select(qInboundErrorInfo)
|
||||
.orderBy(qInboundErrorInfo.erroccurhms.desc())
|
||||
.offset(pageable.getOffset())
|
||||
.limit(pageable.getPageSize())
|
||||
.fetch();
|
||||
|
||||
return new PageImpl<>(list, pageable, totalCount);
|
||||
|
||||
+3
-3
@@ -336,12 +336,12 @@ public class ApiStatsMinuteService
|
||||
|
||||
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
|
||||
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) {
|
||||
endDateTime = startDateTime.plusMinutes(MAX_MINUTES);
|
||||
endDateTime = startDateTime.plusMinutes(MAX_MINUTES - 1);
|
||||
}
|
||||
} else {
|
||||
endDateTime = startDateTime.plusMinutes(MAX_MINUTES);
|
||||
endDateTime = startDateTime.plusMinutes(MAX_MINUTES - 1);
|
||||
}
|
||||
|
||||
search.setParsedStartDateTime(startDateTime);
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.layoutsync.history;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Convert;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Lob;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import com.eactive.eai.data.converter.LocalDateTimeToStringConverter16;
|
||||
import com.eactive.eai.data.entity.AbstractEntity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
|
||||
@Entity
|
||||
@Table(name = "tseaitr10")
|
||||
@org.hibernate.annotations.Table(appliesTo = "tseaitr10", comment = "레이아웃 연동 이력")
|
||||
public class LayoutSyncHistoryEntity extends AbstractEntity<Long> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@EqualsAndHashCode.Include
|
||||
@Column(nullable = false)
|
||||
@Comment("일련번호")
|
||||
private Long seqno;
|
||||
|
||||
@Column(length = 36)
|
||||
@Comment("연동로그처리일련번호")
|
||||
private String logprcssseqno;
|
||||
|
||||
@Column(length = 100)
|
||||
@Comment("서비스명")
|
||||
private String servicename;
|
||||
|
||||
@Column(length = 20)
|
||||
@Comment("커맨드")
|
||||
private String command;
|
||||
|
||||
@Column(length = 50)
|
||||
@Comment("레이아웃 명")
|
||||
private String loutname;
|
||||
|
||||
@Lob
|
||||
@Comment("수신 데이터")
|
||||
private String recvdata;
|
||||
|
||||
@Column(length = 16)
|
||||
@Comment("연동 시각")
|
||||
@Convert(converter = LocalDateTimeToStringConverter16.class)
|
||||
private LocalDateTime recvamndhms;
|
||||
|
||||
@Column(length = 1)
|
||||
@Comment("처리 결과")
|
||||
private String prcssrslt;
|
||||
|
||||
@Column(length = 4000)
|
||||
@Comment("처리 결과 메세지")
|
||||
private String prcssrsltcmnt;
|
||||
|
||||
@Override
|
||||
public @NonNull Long getId() {
|
||||
return seqno;
|
||||
}
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.layoutsync.history;
|
||||
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
|
||||
interface LayoutSyncHistoryRepository extends BaseRepository<LayoutSyncHistoryEntity, Long> {
|
||||
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.layoutsync.history;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||
import com.eactive.eai.rms.onl.manage.rule.layoutsync.history.ui.LayoutSyncHistoryUI;
|
||||
import com.eactive.eai.rms.onl.manage.rule.layoutsync.history.ui.LayoutSyncHistoryUISearch;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class LayoutSyncHistoryService extends AbstractDataService<LayoutSyncHistoryEntity, Long, LayoutSyncHistoryRepository> {
|
||||
|
||||
private static final DateTimeFormatter FMT_YYYYMMDD = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
private static final DateTimeFormatter FMT_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public Page<LayoutSyncHistoryUI> selectList(Pageable pageable, LayoutSyncHistoryUISearch search) {
|
||||
QLayoutSyncHistoryEntity q = QLayoutSyncHistoryEntity.layoutSyncHistoryEntity;
|
||||
JPAQuery<?> query = getJPAQueryFactory().from(q);
|
||||
|
||||
if (StringUtils.isNotBlank(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())) {
|
||||
query.where(q.recvamndhms.goe(parseYYYYMMDD(search.getSearchStartDate()).atStartOfDay()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchEndDate())) {
|
||||
query.where(q.recvamndhms.loe(parseYYYYMMDD(search.getSearchEndDate()).atTime(23, 59, 59)));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchPrcssRslt())) {
|
||||
query.where(q.prcssrslt.eq(search.getSearchPrcssRslt()));
|
||||
}
|
||||
|
||||
long totalCount = query.select(q.count()).fetchOne();
|
||||
|
||||
if (pageable.isPaged()) {
|
||||
query.limit(pageable.getPageSize()).offset(pageable.getOffset());
|
||||
}
|
||||
|
||||
List<LayoutSyncHistoryEntity> entities = query.select(q)
|
||||
.orderBy(q.recvamndhms.desc(), q.seqno.desc())
|
||||
.fetch();
|
||||
|
||||
List<LayoutSyncHistoryUI> rows = entities.stream().map(this::convertToUI).collect(Collectors.toList());
|
||||
|
||||
return new PageImpl<>(rows, pageable, totalCount);
|
||||
}
|
||||
|
||||
public LayoutSyncHistoryUI selectDetail(Long seqno) {
|
||||
return convertToUI(getById(seqno));
|
||||
}
|
||||
|
||||
private static LocalDate parseYYYYMMDD(String yyyyMMdd) {
|
||||
return LocalDate.parse(yyyyMMdd, FMT_YYYYMMDD);
|
||||
}
|
||||
|
||||
private LayoutSyncHistoryUI convertToUI(LayoutSyncHistoryEntity e) {
|
||||
LayoutSyncHistoryUI ui = new LayoutSyncHistoryUI();
|
||||
ui.setSeqno(e.getSeqno());
|
||||
ui.setServiceName(e.getServicename());
|
||||
ui.setCommand(e.getCommand());
|
||||
ui.setLoutName(e.getLoutname());
|
||||
ui.setRecvAmndHms(e.getRecvamndhms() == null ? "" : e.getRecvamndhms().format(FMT_DATETIME));
|
||||
ui.setPrcssRslt(e.getPrcssrslt());
|
||||
ui.setPrcssRsltCmnt(e.getPrcssrsltcmnt());
|
||||
ui.setRecvData(e.getRecvdata());
|
||||
return ui;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,8 @@ public class EAILogDto {
|
||||
|
||||
public EAILogDto(EAILogId eaiLogId, String trackasiskey1ctnt, String trackasiskey2ctnt, String keymgtmsgctnt,
|
||||
String eaisvcname, String extbizcd, String refkey, String clientname,
|
||||
String orgname, String msgprcssyms, String companycode, String eaierrctnt, String eaiSvcDesc) {
|
||||
String orgname, String msgprcssyms, String companycode, String eaierrctnt,
|
||||
String eaierrcd, String eaiSvcDesc) {
|
||||
this.eaiLog = new EAILog();
|
||||
this.eaiLog.setId(eaiLogId);
|
||||
this.eaiLog.setTrackasiskey1ctnt(trackasiskey1ctnt);
|
||||
@@ -33,6 +34,7 @@ public class EAILogDto {
|
||||
this.eaiLog.setMsgprcssyms(msgprcssyms);
|
||||
this.eaiLog.setCompanycode(companycode);
|
||||
this.eaiLog.setEaierrctnt(eaierrctnt);
|
||||
this.eaiLog.setEaierrcd(eaierrcd);
|
||||
this.eaiSvcDesc = eaiSvcDesc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.eactive.eai.data.entity.onl.logger.EAILogId;
|
||||
import com.eactive.eai.data.entity.onl.logger.QEAILog;
|
||||
import com.eactive.eai.data.entity.onl.message.QEAIMessageEntity;
|
||||
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.QUserBusiness;
|
||||
import com.eactive.eai.rms.data.entity.onl.code.CodeService;
|
||||
import com.querydsl.core.Tuple;
|
||||
import com.querydsl.core.types.Projections;
|
||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
@@ -12,6 +13,7 @@ import com.querydsl.core.types.dsl.EntityPathBase;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -27,8 +29,15 @@ import java.util.stream.Collectors;
|
||||
@Repository
|
||||
public class EAILogRepositoryImpl implements EAILogRepository {
|
||||
|
||||
// 업무(BIZ) 오류코드 그룹 - 해당 코드는 시스템 에러가 아닌 정상적인 업무 거부 응답이므로
|
||||
// "에러로그" 검색 조건에서는 제외한다.
|
||||
private static final String ERRCODE_BIZ_GROUP = "ERRCODE_BIZ";
|
||||
|
||||
private EntityManager em;
|
||||
|
||||
|
||||
@Autowired
|
||||
private CodeService codeService;
|
||||
|
||||
@PersistenceContext
|
||||
public void setEm(EntityManager em) {
|
||||
this.em = em;
|
||||
@@ -57,7 +66,7 @@ public class EAILogRepositoryImpl implements EAILogRepository {
|
||||
qeaiLog.id, qeaiLog.trackasiskey1ctnt, qeaiLog.trackasiskey2ctnt, qeaiLog.keymgtmsgctnt,
|
||||
qeaiLog.eaisvcname, qeaiLog.extbizcd, qeaiLog.refkey, qeaiLog.clientname,
|
||||
qeaiLog.orgname, qeaiLog.msgprcssyms, qeaiLog.companycode, qeaiLog.eaierrctnt,
|
||||
qeaiMessageEntity.eaisvcdesc))
|
||||
qeaiLog.eaierrcd, qeaiMessageEntity.eaisvcdesc))
|
||||
.from(entityPathBase)
|
||||
.leftJoin(qeaiMessageEntity)
|
||||
.on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname))
|
||||
@@ -102,9 +111,15 @@ public class EAILogRepositoryImpl implements EAILogRepository {
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(eaiLogSearch.getSearchGuid())) {
|
||||
query
|
||||
.where(qeaiLog.pguid.eq(eaiLogSearch.getSearchGuid()))
|
||||
.where(qeaiLog.id.logprcssserno.eq(eaiLogSearch.getSearchLogPrcssSerno()));
|
||||
query.where(qeaiLog.pguid.eq(eaiLogSearch.getSearchGuid()));
|
||||
|
||||
// 처리단계(logprcssserno)를 명시적으로 선택한 경우에만 추가로 좁힌다.
|
||||
// 하나의 GUID는 처리단계(100/200/300/400)별로 여러 로그 행에 걸쳐 존재하므로,
|
||||
// 선택하지 않았으면 해당 GUID의 모든 단계를 조회한다.
|
||||
if (StringUtils.isNotBlank(eaiLogSearch.getSearchLogPrcssSerno())) {
|
||||
query.where(qeaiLog.id.logprcssserno.eq(eaiLogSearch.getSearchLogPrcssSerno()));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,8 +204,18 @@ public class EAILogRepositoryImpl implements EAILogRepository {
|
||||
.between(eaiLogSearch.getSearchStartTime(), eaiLogSearch.getSearchEndTime()))
|
||||
.where(
|
||||
qeaiLog.eaierrcd.trim().isNotEmpty()
|
||||
)
|
||||
.distinct();
|
||||
);
|
||||
|
||||
// ERRCODE_BIZ(업무 오류) 코드는 시스템 에러가 아니므로 "에러로그" 검색 대상에서 제외
|
||||
List<String> bizErrCodes = codeService.findByIdCodegroup(ERRCODE_BIZ_GROUP).stream()
|
||||
.map(code -> code.getId().getCode())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!bizErrCodes.isEmpty()) {
|
||||
errorInstancesSubQuery.where(qeaiLog.eaierrcd.notIn(bizErrCodes));
|
||||
}
|
||||
|
||||
errorInstancesSubQuery.distinct();
|
||||
|
||||
// 찾은 eaisevrinstncname에 해당하는 모든 로그 조회
|
||||
query.where(qeaiLog.id.eaisvcserno.in(errorInstancesSubQuery));
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.eai.common.util.UUIDGenerator;
|
||||
import com.eactive.eai.data.RollingTable;
|
||||
import com.eactive.eai.data.entity.onl.logger.EAILog;
|
||||
import com.eactive.eai.data.entity.onl.logger.EAILogId;
|
||||
@@ -38,19 +39,36 @@ public class EAILogServiceImpl implements EAILogService {
|
||||
|
||||
@Override
|
||||
public Page<EAILogDto> findAll(Pageable pageable, EAILogSearch eaiLogSearch) {
|
||||
long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis();
|
||||
long time = CommonUtil.getCalendar(resolveTableDate(eaiLogSearch)).getTimeInMillis();
|
||||
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
|
||||
Page<EAILogDto> eaiLogDtos = eaiLogRepository.findAll(eaiLog.getClass(), pageable, eaiLogSearch);
|
||||
return eaiLogDtos;
|
||||
}
|
||||
|
||||
public Map<String, Long> getTransactionCounts(EAILogSearch eaiLogSearch) {
|
||||
long time = CommonUtil.getCalendar(eaiLogSearch.getSearchStartTime()).getTimeInMillis();
|
||||
long time = CommonUtil.getCalendar(resolveTableDate(eaiLogSearch)).getTimeInMillis();
|
||||
EAILog eaiLog = (EAILog) applicationContext.getBean(RollingTable.class, EAILog.class, time);
|
||||
Map<String, Long> detailedCounts = eaiLogRepository.countDetailedTransactions(eaiLog.getClass(), eaiLogSearch);
|
||||
return detailedCounts;
|
||||
}
|
||||
|
||||
// GUID로 조회하는 경우, 대상 롤링 테이블을 검색시간이 아닌 GUID 자체에 포함된 시각으로 특정한다.
|
||||
// GUID 포맷/오프셋은 UUIDGenerator.GUID_DATE_POSITION 기준.
|
||||
private String resolveTableDate(EAILogSearch eaiLogSearch) {
|
||||
String guid = eaiLogSearch.getSearchGuid();
|
||||
|
||||
if (StringUtils.isNotBlank(guid)) {
|
||||
int start = UUIDGenerator.GUID_DATE_POSITION;
|
||||
int end = start + 14; // yyyyMMddHHmmss
|
||||
|
||||
if (guid.length() >= end) {
|
||||
return guid.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
return eaiLogSearch.getSearchStartTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EAILogDto getById(EAILogId eaiLogId) {
|
||||
long time = CommonUtil.getCalendar(eaiLogId.getMsgdpstyms()).getTimeInMillis();
|
||||
|
||||
+8
@@ -20,4 +20,12 @@ public class TransactionTrackingFilterService extends
|
||||
.fetch();
|
||||
}
|
||||
|
||||
public void deleteAllByUserId(String userId) {
|
||||
repository.deleteAll(findAllByUserId(userId));
|
||||
}
|
||||
|
||||
public void saveAll(List<TransactionTrackingFilter> filters) {
|
||||
repository.saveAll(filters);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+113
-44
@@ -22,6 +22,7 @@ import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.apim.portal.apispec.repository.ApiSpecInfoRepository;
|
||||
import com.eactive.apim.portal.common.entity.Auditable;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApi;
|
||||
@@ -48,6 +49,12 @@ import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow;
|
||||
* 검수·게시해야 개발자포탈에 노출되며, {@code PUBLISH}면 탐지 즉시 노출된다.
|
||||
* {@code OFF} 여도 이미 열려있는 장애의 복구 처리(ERROR_END/DELAY_END)는 계속 동작한다.</p>
|
||||
*
|
||||
* <p>개발자포탈 미게시 인터페이스 포함 여부는 {@code djb.apistatus.auto-incident-include-unpublished}
|
||||
* (기본 {@code false}) 로 정한다. 기본값이면 게시 API 만 이슈에 담고,
|
||||
* 게시 API 가 하나도 없는 탐지는 기록하지 않는다. {@code true}면 미게시 인터페이스도
|
||||
* 포함해 건수로 집계된다. 복구 처리는 이 설정과 무관하게
|
||||
* 이미 이슈에 담긴 API 를 대상으로 동작한다.</p>
|
||||
*
|
||||
* <p>점검(CONTROL_*) 은 관리자가 공지사항으로 등록한 점검 일정이 원천이다
|
||||
* ({@code ApiStatusRepository.findApiStatusEvents} 가 INCIDENT 기간을 읽어 CTRL_YN 을 판정).
|
||||
* 따라서 CONTROL 이벤트를 다시 기록하면 순환이 되므로 기록하지 않는다.</p>
|
||||
@@ -58,8 +65,8 @@ public class ApiStatusDetectionService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class);
|
||||
|
||||
public static final String EVENT_CONTROL_START = "CONTROL_START";
|
||||
public static final String EVENT_CONTROL_END = "CONTROL_END";
|
||||
public static final String EVENT_CHECK_START = "CHECK_START";
|
||||
public static final String EVENT_CHECK_END = "CHECK_END";
|
||||
public static final String EVENT_ERROR_START = "ERROR_START";
|
||||
public static final String EVENT_ERROR_END = "ERROR_END";
|
||||
public static final String EVENT_DELAY_START = "DELAY_START";
|
||||
@@ -95,6 +102,12 @@ public class ApiStatusDetectionService {
|
||||
/** 오탐이 그대로 고객에게 노출되지 않도록 기본은 검수(DRAFT) */
|
||||
private static final String DEFAULT_AUTO_INCIDENT_MODE = MODE_DRAFT;
|
||||
|
||||
/** 자동 탐지 이슈에 개발자포탈 미게시 인터페이스도 포함할지 여부 - true / false */
|
||||
public static final String KEY_INCLUDE_UNPUBLISHED = "djb.apistatus.auto-incident-include-unpublished";
|
||||
|
||||
/** 포탈 게시 API 장애만 고객 공지 대상이 되도록 기본은 제외(false) */
|
||||
private static final String DEFAULT_INCLUDE_UNPUBLISHED = "false";
|
||||
|
||||
@Autowired
|
||||
private DjbApistatusIncidentRepository incidentRepository;
|
||||
|
||||
@@ -110,17 +123,25 @@ public class ApiStatusDetectionService {
|
||||
@Autowired
|
||||
private ApiStatusRepository apiStatusRepository;
|
||||
|
||||
/** EMSAPP(PTL_API_SPEC_INFO) 조회용. AGWAPP 커넥션으로 도는 apiStatusRepository 로는 못 읽는다 */
|
||||
@Autowired
|
||||
private ApiSpecInfoRepository apiSpecInfoRepository;
|
||||
|
||||
@Autowired
|
||||
private PortalPropertyService portalPropertyService;
|
||||
|
||||
/** 자동 초안 문구 양식 (classpath:apistatus-draft.yml) */
|
||||
/** 공지 문구 양식 (PTL_PROPERTY > classpath:apistatus-draft.yml) */
|
||||
@Autowired
|
||||
private ApiStatusDraftTemplate draftTemplate;
|
||||
|
||||
/** 제목·본문 조립. 관리자 수동 등록 화면과 같은 양식을 쓰기 위해 분리했다 */
|
||||
@Autowired
|
||||
private ApiStatusNoticeComposer noticeComposer;
|
||||
|
||||
/**
|
||||
* 이벤트 감지
|
||||
* @param event "CONTROL_START" : 점검시작
|
||||
"CONTROL_END" : 점검종료
|
||||
* @param event "CHECK_START" : 점검시작
|
||||
"CHECK_END" : 점검종료
|
||||
"ERROR_START" : 장애시작
|
||||
"ERROR_END" : 장애종료
|
||||
"DELAY_START" : 지연시작
|
||||
@@ -154,8 +175,8 @@ public class ApiStatusDetectionService {
|
||||
case EVENT_DELAY_END:
|
||||
markRecovered(event, targets);
|
||||
break;
|
||||
case EVENT_CONTROL_START:
|
||||
case EVENT_CONTROL_END:
|
||||
case EVENT_CHECK_START:
|
||||
case EVENT_CHECK_END:
|
||||
// 점검은 관리자 등록 공지가 원천 - 재기록하지 않음
|
||||
log.debug("점검 이벤트는 장애 데이터로 기록하지 않음: {} {}", event, targets);
|
||||
break;
|
||||
@@ -203,6 +224,19 @@ public class ApiStatusDetectionService {
|
||||
}
|
||||
boolean autoPublish = MODE_PUBLISH.equals(mode);
|
||||
|
||||
// 미게시 인터페이스 제외 설정(false)이면 게시 API 만 남기고, 하나도 없으면 기록하지 않는다
|
||||
if (!resolveIncludeUnpublished()) {
|
||||
List<String> published = filterPublished(apiIds);
|
||||
if (published.isEmpty()) {
|
||||
log.info("게시 API 없음 - 미게시 인터페이스 제외 설정으로 기록하지 않음: {} {}", event, apiIds);
|
||||
return;
|
||||
}
|
||||
if (published.size() < apiIds.size()) {
|
||||
log.info("미게시 인터페이스 제외: {} → {}", apiIds, published);
|
||||
}
|
||||
apiIds = published;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String interfaceId = makeInterfaceId(event, apiIds, now);
|
||||
|
||||
@@ -309,6 +343,27 @@ public class ApiStatusDetectionService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동 탐지 이슈에 개발자포탈 미게시 인터페이스도 포함할지 여부.
|
||||
*
|
||||
* <p>PTL_PROPERTY({@code Portal} / {@code djb.apistatus.auto-incident-include-unpublished}) 를 읽는다.
|
||||
* 최초 접근 시 기본값으로 row 가 자동 생성된다. 기본 {@code false} - 게시 API 만 이슈에 담는다.
|
||||
* 명시적으로 {@code true}/{@code Y} 를 설정한 경우에만 미게시 인터페이스를 포함하며,
|
||||
* 알 수 없는 값·조회 실패는 기본값 쪽(false)으로 떨어뜨린다.</p>
|
||||
*/
|
||||
private boolean resolveIncludeUnpublished() {
|
||||
try {
|
||||
String value = portalPropertyService.getOrCreateProperty(
|
||||
PROPERTY_GROUP, KEY_INCLUDE_UNPUBLISHED, DEFAULT_INCLUDE_UNPUBLISHED,
|
||||
"API 상태 자동 탐지 이슈에 포탈 미게시 인터페이스 포함 여부(true/false)");
|
||||
String normalized = StringUtils.trimToEmpty(value);
|
||||
return "true".equalsIgnoreCase(normalized) || "Y".equalsIgnoreCase(normalized);
|
||||
} catch (Exception e) {
|
||||
log.warn("미게시 인터페이스 포함 여부 조회 실패 - 기본값 {} 적용", DEFAULT_INCLUDE_UNPUBLISHED, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeIntoOpenIncident(DjbApistatusIncident incident, String event, IncidentKind kind,
|
||||
List<String> apiIds, LocalDateTime now, boolean autoPublish) {
|
||||
Long incidentId = incident.getIncidentId();
|
||||
@@ -534,14 +589,47 @@ public class ApiStatusDetectionService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 게시 인터페이스 판정. 조회에 실패하면 전부 비게시로 보아 ID·명칭 노출을 막는다. */
|
||||
/**
|
||||
* 게시 인터페이스 ID 집합 조회. 실패 처리는 호출부 정책에 맡긴다.
|
||||
*
|
||||
* <p>판정 조건 두 개가 서로 다른 스키마에 있어 조회를 나눈다 - API 그룹 편성은 AGWAPP,
|
||||
* 스펙 존재는 EMSAPP(PTL_API_SPEC_INFO). 한 쿼리로 조인하면 tenant=APIGW 커넥션이
|
||||
* EMSAPP 를 못 봐서 ORA-00942 가 난다.</p>
|
||||
*/
|
||||
private Set<String> lookupPublishedApiIds(List<String> apiIds) {
|
||||
List<String> groupedIds = apiStatusRepository.findDisplayedGroupApiIds(apiIds);
|
||||
return groupedIds.isEmpty()
|
||||
? Collections.emptySet()
|
||||
: new HashSet<>(apiSpecInfoRepository.findApiIdsIn(groupedIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시 API 만 남긴다 (미게시 인터페이스 제외 설정용). 판정에 실패하면 전체를 유지한다 -
|
||||
* 제외 판단이 안 되는 상황에서 진짜 장애 기록까지 버리는 쪽이 더 위험하고,
|
||||
* 명칭 노출은 표기 단계({@link #splitPublished})가 따로 막는다.
|
||||
*/
|
||||
private List<String> filterPublished(List<String> apiIds) {
|
||||
try {
|
||||
Set<String> publishedIds = lookupPublishedApiIds(apiIds);
|
||||
return apiIds.stream()
|
||||
.filter(publishedIds::contains)
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
log.warn("게시 API 판정 실패 - 미게시 인터페이스 제외 없이 전체 포함", e);
|
||||
return apiIds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시 인터페이스 판정. 조회에 실패하면 전부 비게시로 보아 ID·명칭 노출을 막는다.
|
||||
*/
|
||||
private ApiSplit splitPublished(List<String> apiIds) {
|
||||
if (apiIds == null || apiIds.isEmpty()) {
|
||||
return new ApiSplit(Collections.emptyList(), 0);
|
||||
}
|
||||
Set<String> publishedIds;
|
||||
try {
|
||||
publishedIds = new HashSet<>(apiStatusRepository.findPublishedApiIds(apiIds));
|
||||
publishedIds = lookupPublishedApiIds(apiIds);
|
||||
} catch (Exception e) {
|
||||
// 실패 시 노출하는 쪽으로 기울면 내부 인터페이스가 고객 화면에 새어 나간다
|
||||
log.warn("게시 API 판정 실패 - 전부 GW 인터페이스로 묶어 표기", e);
|
||||
@@ -558,7 +646,7 @@ public class ApiStatusDetectionService {
|
||||
|
||||
/** "GW 인터페이스 3건" - 게시되지 않은 인터페이스 묶음 표기 */
|
||||
private String hiddenLabel(int hiddenCount) {
|
||||
return draftTemplate.text(ApiStatusDraftTemplate.GW_LABEL, "count", String.valueOf(hiddenCount));
|
||||
return noticeComposer.hiddenLabel(hiddenCount);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -583,16 +671,17 @@ public class ApiStatusDetectionService {
|
||||
*/
|
||||
private String buildTitle(String titleKeyword, List<String> apiIds, Map<String, String> apiNames) {
|
||||
ApiSplit split = splitPublished(apiIds);
|
||||
if (split.published.isEmpty()) {
|
||||
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_HIDDEN_ONLY,
|
||||
"gw", hiddenLabel(apiIds.size()), "keyword", titleKeyword);
|
||||
return noticeComposer.buildTitle(titleKeyword, labelsOf(split.published, apiNames),
|
||||
split.hiddenCount, apiIds.size());
|
||||
}
|
||||
|
||||
/** 인터페이스 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);
|
||||
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);
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -606,39 +695,19 @@ public class ApiStatusDetectionService {
|
||||
*/
|
||||
private PortalNotice createNotice(String title, String summary, List<String> apiIds,
|
||||
Map<String, String> apiNames, LocalDateTime now, boolean publish) {
|
||||
StringBuilder detail = new StringBuilder();
|
||||
detail.append(draftTemplate.text(publish
|
||||
String lead = draftTemplate.text(publish
|
||||
? ApiStatusDraftTemplate.BODY_LEAD_PUBLISH
|
||||
: ApiStatusDraftTemplate.BODY_LEAD_DRAFT,
|
||||
"summary", summary));
|
||||
"summary", summary);
|
||||
|
||||
// 게시된 API 만 이름으로 적고(인터페이스 ID 는 넣지 않는다) 나머지는 건수로 묶는다.
|
||||
// 공지 본문은 저장 시점에 굳는 HTML 이라 나중에 사용자별로 가릴 수 없다.
|
||||
ApiSplit split = splitPublished(apiIds);
|
||||
detail.append("<p><b>")
|
||||
.append(draftTemplate.text(ApiStatusDraftTemplate.BODY_AFFECTED_HEADING))
|
||||
.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>");
|
||||
}
|
||||
String detail = noticeComposer.buildBody(
|
||||
lead, labelsOf(split.published, apiNames), split.hiddenCount, !publish);
|
||||
|
||||
PortalNotice notice = new PortalNotice();
|
||||
notice.setId(null);
|
||||
notice.setNoticeSubject(StringUtils.abbreviate(title, NOTICE_SUBJECT_MAX_LENGTH));
|
||||
notice.setNoticeDetail(detail.toString());
|
||||
notice.setNoticeDetail(detail);
|
||||
notice.setNoticeType(NOTICE_TYPE_INCIDENT);
|
||||
notice.setUseYn(publish ? "Y" : "N");
|
||||
notice.setFixYn("N");
|
||||
|
||||
@@ -11,21 +11,32 @@ import javax.annotation.PostConstruct;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
|
||||
/**
|
||||
* API Status 자동 탐지 초안 양식({@code classpath:apistatus-draft.yml}).
|
||||
* API Status 공지 양식. 자동 탐지 초안과 관리자 수동 등록이 같은 문구를 쓴다.
|
||||
*
|
||||
* <p>자동 생성 공지의 제목·본문·타임라인 문구를 코드가 아닌 설정으로 관리한다.
|
||||
* admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없으므로 SnakeYAML 로 직접 읽는다
|
||||
* ({@code <context:property-placeholder>} 는 properties 전용이고, 검수 섹션이 리스트라
|
||||
* properties 로는 인덱스 키로 흩어진다).</p>
|
||||
* <p>문구 출처는 우선순위 3단이다.</p>
|
||||
* <ol>
|
||||
* <li>{@code PTL_PROPERTY} (그룹 {@link #PROPERTY_GROUP}, 키 {@link #PROPERTY_PREFIX} + 문구키)
|
||||
* - 운영 중 포탈 프로퍼티 관리 화면에서 재배포 없이 고칠 수 있다.</li>
|
||||
* <li>{@code classpath:apistatus-draft.yml} - 배포본에 남기는 문구 원본.</li>
|
||||
* <li>코드 {@link #DEFAULTS} - 위 둘이 모두 없을 때.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>파일이 없거나 파싱에 실패하면 {@link #DEFAULTS} 로 동작한다 - 문구 설정 문제로
|
||||
* 장애 탐지 자체가 멈추면 안 되기 때문이다. 개별 키가 비어도 같은 이유로 기본값으로 떨어진다.</p>
|
||||
* <p>yml 을 SnakeYAML 로 직접 읽는 이유: admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없고
|
||||
* ({@code <context:property-placeholder>} 는 properties 전용), 검수 섹션이 리스트라 properties 로는
|
||||
* 인덱스 키로 흩어진다.</p>
|
||||
*
|
||||
* <p>DB 조회는 60초 스냅샷으로 캐시한다 - 공지 한 건을 만드는 데 {@link #text} 가 수십 번 불리므로
|
||||
* 매번 조회하면 안 된다. 조회에 실패하거나 개별 키가 비어 있으면 그 아래 단계 문구로 떨어진다.
|
||||
* 문구 설정 문제로 장애 탐지 자체가 멈추면 안 되기 때문이다.</p>
|
||||
*/
|
||||
@Component
|
||||
public class ApiStatusDraftTemplate {
|
||||
@@ -34,6 +45,15 @@ public class ApiStatusDraftTemplate {
|
||||
|
||||
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";
|
||||
|
||||
@@ -41,12 +61,21 @@ public class ApiStatusDraftTemplate {
|
||||
public static final String TITLE_MULTI = "title.multi";
|
||||
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_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_FILLED_TEXT = "body.filled-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_API_ADDED = "timeline.api-added";
|
||||
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_PART = "timeline.recovered-part";
|
||||
|
||||
/** 검수 항목 프로퍼티 구분자 - 항목 사이 */
|
||||
private static final String SECTION_DELIMITER = ";;";
|
||||
|
||||
/** 검수 항목 프로퍼티 구분자 - 제목과 힌트 사이 */
|
||||
private static final String SECTION_FIELD_DELIMITER = "|";
|
||||
|
||||
/** yml 을 못 읽었을 때 쓰는 기본 문구 (기존 하드코딩과 동일) */
|
||||
private static final Map<String, String> DEFAULTS;
|
||||
|
||||
/** 프로퍼티 최초 등록 시 함께 적는 설명 (관리 화면에서 치환자 의미를 보여준다) */
|
||||
private static final Map<String, String> DESCRIPTIONS;
|
||||
|
||||
static {
|
||||
Map<String, String> defaults = new LinkedHashMap<>();
|
||||
defaults.put(GW_LABEL, "GW 인터페이스 {count}건");
|
||||
defaults.put(TITLE_SINGLE, "[자동감지] {first} API {keyword}");
|
||||
defaults.put(TITLE_MULTI, "[자동감지] {first} 외 {rest}종 API {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_DRAFT, "<p>{summary} 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.</p>");
|
||||
defaults.put(BODY_LEAD_MANUAL, "<p>[작성 필요] 장애 개요를 입력해 주십시오.</p>");
|
||||
defaults.put(BODY_AFFECTED_HEADING, "영향 API");
|
||||
defaults.put(BODY_FILLED_TEXT, "확인 중입니다.");
|
||||
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_PART, "일부 복구 확인 ({event})\n{affected}");
|
||||
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 {
|
||||
}
|
||||
});
|
||||
|
||||
/** 점(.) 으로 평탄화한 문구 맵 */
|
||||
private Map<String, String> texts = DEFAULTS;
|
||||
private List<Section> sections = DEFAULT_SECTIONS;
|
||||
@Autowired(required = false)
|
||||
private PortalPropertyService portalPropertyService;
|
||||
|
||||
/** 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
|
||||
public void load() {
|
||||
try (InputStream in = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||
@@ -127,14 +206,17 @@ public class ApiStatusDraftTemplate {
|
||||
|
||||
Map<String, String> loaded = new LinkedHashMap<>(DEFAULTS);
|
||||
flatten("", draft, loaded);
|
||||
this.texts = loaded;
|
||||
this.sections = readSections(draft);
|
||||
log.info("초안 양식 로딩 완료: {} (섹션 {}개)", RESOURCE, sections.size());
|
||||
this.baselineTexts = loaded;
|
||||
this.baselineSections = readSections(draft);
|
||||
log.info("초안 양식 로딩 완료: {} (섹션 {}개)", RESOURCE, baselineSections.size());
|
||||
} catch (Exception e) {
|
||||
// 문구 설정 문제로 장애 탐지가 멈추면 안 된다
|
||||
log.warn("{} 로딩 실패 - 기본 초안 양식 사용", RESOURCE, e);
|
||||
this.texts = DEFAULTS;
|
||||
this.sections = DEFAULT_SECTIONS;
|
||||
this.baselineTexts = DEFAULTS;
|
||||
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) {
|
||||
ensureFresh();
|
||||
String template = texts.get(key);
|
||||
if (template == null) {
|
||||
template = baselineTexts.get(key);
|
||||
}
|
||||
if (template == null) {
|
||||
template = DEFAULTS.get(key);
|
||||
}
|
||||
@@ -159,9 +245,124 @@ public class ApiStatusDraftTemplate {
|
||||
}
|
||||
|
||||
public List<Section> getSections() {
|
||||
ensureFresh();
|
||||
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)는 별도로 읽는다. */
|
||||
|
||||
@@ -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,6 +1,8 @@
|
||||
package com.eactive.eai.rms.ext.djb.apistatus;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
@@ -17,8 +19,8 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
||||
/**
|
||||
* API 상태 판단. 정상(N), 점검(C), 지연(D), 장애(E)
|
||||
*
|
||||
* 1. 정상/지연/장애 -> 점검중 : CONTROL_START
|
||||
* 2. 점검 -> 점검X : CONTROL_END
|
||||
* 1. 정상/지연/장애 -> 점검중 : CHECK_START
|
||||
* 2. 점검 -> 점검X : CHECK_END
|
||||
* 3. 정상/지연 -> 장애 : ERROR_START
|
||||
* 4. 장애 -> 장애X : ERROR_END
|
||||
* 5. 정상 -> 지연 : DELAY_START
|
||||
@@ -34,8 +36,8 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
||||
+ " FROM ("
|
||||
+ " SELECT EAISVCNAME"
|
||||
+ " , STATUS_CODE AS PREV_STATUS_CODE "
|
||||
+ " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CONTROL_START'"
|
||||
+ " WHEN STATUS_CODE = 'C' AND CTRL_YN = 'N' THEN 'CONTROL_END'"
|
||||
+ " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CHECK_START'"
|
||||
+ " WHEN STATUS_CODE = 'C' AND CTRL_YN = 'N' THEN 'CHECK_END'"
|
||||
+ " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'"
|
||||
+ " WHEN STATUS_CODE = 'E' AND ERR_YN = 'N' THEN 'ERROR_END'"
|
||||
+ " WHEN STATUS_CODE = 'N' AND DELAY_YN = 'Y' THEN 'DELAY_START'"
|
||||
@@ -45,7 +47,7 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
||||
+ " SELECT A.EAISVCNAME"
|
||||
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
|
||||
// 점검(KIND='MAINTENANCE')만 CTRL_YN 대상이다. KIND 를 안 걸면 관리자가 시작~종료 시각을
|
||||
// 넣어 등록한 장애 공지까지 '점검중'으로 오판해 CONTROL_START 가 발생한다.
|
||||
// 넣어 등록한 장애 공지까지 '점검중'으로 오판해 CHECK_START 가 발생한다.
|
||||
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
|
||||
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
|
||||
+ " AND Y.KIND = 'MAINTENANCE'"
|
||||
@@ -82,6 +84,36 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
||||
@Param("delayAvgRespTime") int delayAvgRespTime
|
||||
);
|
||||
|
||||
/**
|
||||
* 현재 저장된 STATUS_CODE가 prevStatusCode와 일치할 때만 newStatusCode로 갱신한다 (CAS).
|
||||
*
|
||||
* <p>{@code ApiStatusMonitorJob}이 둘 이상의 인스턴스에서 겹쳐 실행될 수 있는 경우를 대비한
|
||||
* 방어다. 두 실행이 동시에 같은 API의 상태 전이(event)를 감지해도, 먼저 커밋한 쪽만
|
||||
* 실제로 갱신에 성공(영향 1행)하고 나머지는 0행이 되어 중복 알림 발송을 막을 수 있다.</p>
|
||||
*
|
||||
* @return 갱신된 행 수(0 또는 1). 0이면 이미 다른 실행이 선점했거나 최초 기록(행 없음)이다.
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE ApiStatus a SET a.statusCode = :newStatusCode, a.modifiedBy = 'SCHEDULER', a.modifiedDate = CURRENT_TIMESTAMP " +
|
||||
"WHERE a.eaisvcname = :id AND a.statusCode = :prevStatusCode")
|
||||
int updateStatusCodeIfMatches(@Param("id") String id,
|
||||
@Param("prevStatusCode") String prevStatusCode,
|
||||
@Param("newStatusCode") String newStatusCode);
|
||||
|
||||
/**
|
||||
* API_STATUS 최초 기록용 INSERT. {@link #updateStatusCodeIfMatches}가 0행을 갱신했을 때
|
||||
* (해당 API의 상태 row가 아직 없을 때) 시도한다. 그 사이 다른 실행이 먼저 INSERT했다면
|
||||
* PK(EAISVCNAME) 제약 위반으로 실패하며, 이는 선점 실패로 처리한다.
|
||||
*/
|
||||
@Modifying
|
||||
@Query(nativeQuery = true, value =
|
||||
"INSERT INTO API_STATUS (EAISVCNAME, STATUS_CODE, MODIFIED_BY, MODIFIED_DATE) " +
|
||||
"VALUES (:id, :statusCode, :modifiedBy, :modifiedDate)")
|
||||
void insertIfAbsent(@Param("id") String id,
|
||||
@Param("statusCode") String statusCode,
|
||||
@Param("modifiedBy") String modifiedBy,
|
||||
@Param("modifiedDate") LocalDateTime modifiedDate);
|
||||
|
||||
/**
|
||||
* 판정 구간에 트래픽이 있는 API 의 오류율·지연 판정값. 개발 모드(-Deai.systemmode=D) 진단 로그 전용.
|
||||
*
|
||||
@@ -147,22 +179,25 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
||||
List<ApiNameRow> findApiNames(@Param("apiIds") Collection<String> apiIds);
|
||||
|
||||
/**
|
||||
* 개발자포탈에 게시되는 인터페이스(= API) 만 추린다.
|
||||
* 노출 중인 API 그룹에 편성된 인터페이스 ID.
|
||||
*
|
||||
* <p>GW 인터페이스는 전부 탐지 대상이지만 개발자포탈에 노출되는 것은 그 일부다.
|
||||
* 자동 초안(제목·공지 본문·타임라인)에 게시되지 않은 인터페이스 ID 가 그대로 박히면
|
||||
* 고객 화면에 내부 인터페이스가 드러나므로, 여기서 걸러 나머지는 건수로만 표기한다.</p>
|
||||
*
|
||||
* <p>판정 기준은 포털 "오픈 API" 목록과 같은 두 조건이다 - 노출 중인 API 그룹에 편성돼 있고
|
||||
* PTL_API_SPEC_INFO 에 스펙이 있을 것. 역할·소속에 따른 사용자별 공개 범위는 admin 에
|
||||
* 사용자 컨텍스트가 없어 적용하지 않는다(= 포털 화면이 한 번 더 좁힌다).</p>
|
||||
* <p>게시 판정은 포털 "오픈 API" 목록과 같은 두 조건이다 - 노출 중인 API 그룹에 편성돼 있고
|
||||
* PTL_API_SPEC_INFO 에 스펙이 있을 것. 이 쿼리는 앞 조건(AGWAPP)만 본다. 뒤 조건은
|
||||
* EMSAPP 스키마라 {@code ApiSpecInfoRepository.findApiIdsIn} 으로 따로 조회해 교집합을 잡는다
|
||||
* - 이 리포지토리는 tenant=APIGW(AGWAPP) 커넥션으로 도는 탓에 한 쿼리로 조인하면 ORA-00942 다.</p>
|
||||
*
|
||||
* <p>역할·소속에 따른 사용자별 공개 범위는 admin 에 사용자 컨텍스트가 없어 적용하지 않는다
|
||||
* (= 포털 화면이 한 번 더 좁힌다).</p>
|
||||
*/
|
||||
@Query(nativeQuery = true, value =
|
||||
" SELECT DISTINCT GA.API_ID"
|
||||
+ " FROM API_GROUP_API GA"
|
||||
+ " JOIN API_GROUP G ON G.ID = GA.API_GROUP_ID"
|
||||
+ " WHERE GA.API_ID IN (:apiIds)"
|
||||
+ " AND G.DISPLAY_YN = '1'"
|
||||
+ " AND EXISTS (SELECT 1 FROM EMSAPP.PTL_API_SPEC_INFO S WHERE S.API_ID = GA.API_ID)")
|
||||
List<String> findPublishedApiIds(@Param("apiIds") Collection<String> apiIds);
|
||||
+ " AND G.DISPLAY_YN = '1'")
|
||||
List<String> findDisplayedGroupApiIds(@Param("apiIds") Collection<String> apiIds);
|
||||
}
|
||||
@@ -2,21 +2,29 @@ package com.eactive.eai.rms.ext.djb.apistatus;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.data.entity.man.role.Role;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiErrorRateRow;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
|
||||
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
||||
|
||||
@@ -35,8 +43,24 @@ public class ApiStatusService {
|
||||
|
||||
@Autowired
|
||||
private ApiStatusDetectionService apiStatusDectionService;
|
||||
|
||||
|
||||
|
||||
@Autowired
|
||||
private MonitoringContext monitoringContext;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("transactionManagerForEMS")
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
/** {@link #claimStatusChange} 의 최초 INSERT 시도를 바깥 트랜잭션과 분리하기 위한 템플릿 (REQUIRES_NEW) */
|
||||
private TransactionTemplate insertTransactionTemplate;
|
||||
|
||||
@PostConstruct
|
||||
private void initTransactionTemplate() {
|
||||
this.insertTransactionTemplate = new TransactionTemplate(transactionManager);
|
||||
this.insertTransactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
}
|
||||
|
||||
|
||||
public void updateApiStatus(HashMap<String, String> param) {
|
||||
|
||||
int errorRate = Integer.parseInt(param.get("errorRate"));
|
||||
@@ -60,16 +84,16 @@ public class ApiStatusService {
|
||||
String newStatusCode = resolveStatusCode(event.getEvent());
|
||||
if (newStatusCode == null) continue;
|
||||
|
||||
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
|
||||
// 인스턴스가 둘 이상 동시에 이 job을 돌릴 수 있으므로, CAS로 선점(claim)에 성공한 경우만 처리한다.
|
||||
// (그렇지 않으면 두 인스턴스가 같은 상태 전이를 각자 감지해 알림을 중복 발송하게 된다)
|
||||
if (!claimStatusChange(event.getEaisvcname(), event.getPrevStatusCode(), newStatusCode)) {
|
||||
log.debug("API 상태 변경 선점 실패(다른 인스턴스가 이미 처리) - {}", event.getEaisvcname());
|
||||
continue;
|
||||
}
|
||||
|
||||
apiStatus.setEaisvcname(event.getEaisvcname());
|
||||
apiStatus.setStatusCode(newStatusCode);
|
||||
|
||||
apiStatusRepository.saveAndFlush(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT (즉시 flush)
|
||||
|
||||
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
|
||||
|
||||
|
||||
|
||||
|
||||
//이벤트별로 api분리 저장
|
||||
List<String> apis = (List<String>)eventMap.get(event.getEvent());
|
||||
if (apis == null) {
|
||||
@@ -86,18 +110,49 @@ public class ApiStatusService {
|
||||
String event = entry.getKey();
|
||||
List<String> apiIds = entry.getValue();
|
||||
|
||||
HashMap<String, Object> params = getSwingChatMessage(event, apiIds);
|
||||
ums.send("api-monitor", MessageCode.API_STATUS_CHANGED, params);
|
||||
//내부직원 스윙챗 발송
|
||||
if (monitoringContext.getBooleanProperty(MonitoringContext.DJB_UMS_MESSENGER_APIMONITOR_ENABLED, true)) {
|
||||
HashMap<String, Object> params = getSwingChatMessage(event, apiIds);
|
||||
ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, params);
|
||||
}
|
||||
|
||||
//제휴사 웹훅 발송
|
||||
ums.sendWebhook(event, apiIds);
|
||||
if (monitoringContext.getBooleanProperty(MonitoringContext.RMS_WEBHOOK_ENABLED, true)) {
|
||||
ums.sendWebhook(event, apiIds);
|
||||
}
|
||||
|
||||
//개발자포탈 API상태모니터링 화면을 위한 처리
|
||||
apiStatusDectionService.detect(event, apiIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 현재 저장된 상태가 prevStatusCode와 일치할 때만 newStatusCode로 갱신한다 (CAS).
|
||||
*
|
||||
* <p>갱신 대상 행이 없으면(해당 API 최초 기록) INSERT를 시도한다. 그 사이 다른 인스턴스가
|
||||
* 먼저 INSERT했다면 PK 제약 위반이 발생하는데, 이를 선점 실패로 처리한다. 이 INSERT 시도는
|
||||
* 실패해도 {@code updateApiStatus}의 바깥 트랜잭션 전체가 rollback-only가 되지 않도록
|
||||
* 별도 트랜잭션(REQUIRES_NEW)으로 격리한다.</p>
|
||||
*
|
||||
* @return 이번 실행이 상태 변경을 선점했으면 true
|
||||
*/
|
||||
private boolean claimStatusChange(String eaisvcname, String prevStatusCode, String newStatusCode) {
|
||||
int updated = apiStatusRepository.updateStatusCodeIfMatches(eaisvcname, prevStatusCode, newStatusCode);
|
||||
if (updated == 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
insertTransactionTemplate.execute(status -> {
|
||||
apiStatusRepository.insertIfAbsent(eaisvcname, newStatusCode, "SCHEDULER", LocalDateTime.now());
|
||||
return null;
|
||||
});
|
||||
return true;
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 판정 구간에 거래가 있는 API 의 오류율·지연 판정값을 debug 로그로 남긴다.
|
||||
* 개발 모드(-Deai.systemmode=D) 에서만 동작한다.
|
||||
@@ -179,8 +234,8 @@ public class ApiStatusService {
|
||||
|
||||
private String resolveStatusCode(String event) {
|
||||
switch (event) {
|
||||
case "CONTROL_START": return "C"; //점검
|
||||
case "CONTROL_END": return "N"; //정상
|
||||
case "CHECK_START": return "C"; //점검
|
||||
case "CHECK_END": return "N"; //정상
|
||||
case "ERROR_START": return "E"; //장애
|
||||
case "ERROR_END": return "N"; //정상
|
||||
case "DELAY_START": return "D"; //지연
|
||||
@@ -195,10 +250,10 @@ public class ApiStatusService {
|
||||
private HashMap<String, Object> getSwingChatMessage(String event, List<String> apiIds) {
|
||||
String message = "";
|
||||
switch (event) {
|
||||
case "CONTROL_START":
|
||||
case "CHECK_START":
|
||||
message = "API 서비스 점검이 시작되었습니다"; //점검
|
||||
break;
|
||||
case "CONTROL_END":
|
||||
case "CHECK_END":
|
||||
message = "API 서비스 점검이 완료되었습니다"; //정상
|
||||
break;
|
||||
case "ERROR_START":
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
package com.eactive.eai.rms.ext.djb.inflow;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLog;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLogId;
|
||||
|
||||
public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> {
|
||||
|
||||
@Query(nativeQuery = true, value =
|
||||
" SELECT EAISVCNAME" +
|
||||
" , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" +
|
||||
" , COUNT(*) AS CNT" +
|
||||
" FROM TSEAIFR11 A" +
|
||||
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')" +
|
||||
" GROUP BY EAISVCNAME")
|
||||
List<InflowTokenInsufficient> countTokenInsufficient(@Param("rangeMinute") long rangeMinute);
|
||||
}
|
||||
" SELECT COUNT(*)" +
|
||||
" FROM TSEAIFR11" +
|
||||
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')")
|
||||
long countTokenInsufficient(@Param("rangeMinute") long rangeMinute);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.eactive.eai.rms.ext.djb.inflow;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -10,7 +9,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
|
||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
||||
|
||||
|
||||
@@ -22,19 +21,29 @@ public class InflowTokenService {
|
||||
|
||||
@Autowired
|
||||
private InflowTokenRepository inflowTokenRepository;
|
||||
|
||||
|
||||
@Autowired
|
||||
private UmsManager ums;
|
||||
private MonitoringContext monitoringContext;
|
||||
|
||||
@Autowired
|
||||
private UmsManager ums;
|
||||
|
||||
public void checkRecentFails(long rangeMinute) {
|
||||
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
|
||||
|
||||
for (InflowTokenInsufficient info : rows) {
|
||||
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
|
||||
String message = "유량제어 토큰 획득 실패가 발생하였습니다 \n" + info.getEaisvcname();
|
||||
HashMap<String,Object> params = new HashMap<String,Object>();
|
||||
params.put("message", message);
|
||||
ums.send("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, params);
|
||||
if (!monitoringContext.getBooleanProperty(MonitoringContext.DJB_UMS_MESSENGER_APIMONITOR_ENABLED, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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')" +
|
||||
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
|
||||
" , COUNT(EAISVCSERNO)" +
|
||||
" , SUM(CASE WHEN A.ERROR_CODE IS 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 (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.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" +
|
||||
" , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" +
|
||||
" FROM (" +
|
||||
@@ -143,12 +143,12 @@ public class ApiStatsHourlyAggregationJob implements Job {
|
||||
" + TO_NUMBER(SUBSTR(MAX(MSGPRCSSYMS),15,3))" +
|
||||
" - TO_NUMBER(SUBSTR(MIN(MSGPRCSSYMS),15,3))" +
|
||||
" ) AS RESP_TIME" +
|
||||
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" +
|
||||
" , MAX(EAIERRCD) AS ERROR_CODE" +
|
||||
" FROM " + logTableName +
|
||||
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN EAIERRCD ELSE '' END) AS ERROR_CODE" +
|
||||
" FROM " + logTableName + " A " +
|
||||
" WHERE MSGDPSTYMS >= :rangeStart AND MSGDPSTYMS < :rangeEnd" +
|
||||
" GROUP BY EAISVCSERNO" +
|
||||
" ) A LEFT OUTER JOIN TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.USEYN = 'Y' " +
|
||||
" LEFT OUTER JOIN TSEAICM20 C ON A.ERROR_CODE = C.CODE AND C.CODEGROUP = 'ERRCODE_BIZ' AND C.USEYN = 'Y' " +
|
||||
" GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
|
||||
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER";
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
@@ -44,8 +43,12 @@ import com.eactive.eai.rms.onl.common.util.DateUtil;
|
||||
* </ul>
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* <p>동시 실행 허용: DB 커넥션풀 경합/락 대기로 특정 실행이 멈추더라도(트랜잭션 매니저가
|
||||
* 다른 여러 퍼시스턴스 경로가 얽혀 있어 발생 가능) 다음 fire까지 막히지 않도록
|
||||
* {@code @DisallowConcurrentExecution}을 두지 않는다. 상태 판정/알림 로직은
|
||||
* interfaceId 시간창 기반 중복 제거가 있어 겹쳐 실행돼도 안전하다.</p>
|
||||
*/
|
||||
@DisallowConcurrentExecution
|
||||
public class ApiStatusMonitorJob implements Job {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiStatusMonitorJob.class);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.eactive.eai.rms.ext.djb.job;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -13,7 +11,6 @@ import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.quartz.Trigger;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -37,7 +34,7 @@ import com.eactive.eai.rms.onl.common.util.DateUtil;
|
||||
* <p>Job 파라미터 (JobDataMap):</p>
|
||||
* <ul>
|
||||
* <li>
|
||||
* NONE
|
||||
* <b>api.inflow.fail.range_minute</b>: 유량제어 토큰 획득 실패 조회 구간 (단위: 분, 기본값: 5)
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
@@ -48,6 +45,12 @@ public class InflowTokenMonitorJob implements Job {
|
||||
|
||||
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;
|
||||
|
||||
@Override
|
||||
@@ -69,18 +72,19 @@ public class InflowTokenMonitorJob implements Job {
|
||||
|
||||
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||
|
||||
long execMinute = 1; //default 배치 실행주기(분)
|
||||
|
||||
Trigger trigger = context.getTrigger();
|
||||
Date prevFireTime = trigger.getPreviousFireTime();
|
||||
Date nextFireTime = trigger.getNextFireTime();
|
||||
|
||||
//1회 이상 배치가 실행된 경우, 배치 간격을 구하여 유량제어조회 구간 파라미터로 전달한다
|
||||
if (prevFireTime != null && nextFireTime != null) {
|
||||
long diffMillis = nextFireTime.getTime() - prevFireTime.getTime();
|
||||
execMinute = TimeUnit.MILLISECONDS.toMinutes(diffMillis);
|
||||
log.info("실행 주기: {}분", execMinute);
|
||||
}
|
||||
// Job 프로퍼티에서 유량제어 실패 조회 구간(분)을 읽는다. 미지정 시 기본값 사용
|
||||
long execMinute;
|
||||
String rangeMinute = jobDataMap.getString(KEY_API_INFLOW_FAIL_RANGE_MINUTE);
|
||||
if (StringUtils.isEmpty(rangeMinute)) {
|
||||
rangeMinute = DEFAULT_RANGE_MINUTE;
|
||||
}
|
||||
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;
|
||||
try {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.eactive.eai.rms.ext.djb.smsauth;
|
||||
|
||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
|
||||
import com.eactive.eai.rms.common.login.ChangePasswordResponseDto;
|
||||
import com.eactive.eai.rms.common.login.LoginVo;
|
||||
import com.eactive.eai.rms.common.login.MainController;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -70,6 +71,7 @@ public class SmsAuthController implements InterceptorSkipController {
|
||||
.maskedPhone(getSmsAuthService().maskPhoneNumber(userInfo.getCphnno()))
|
||||
.expiresAt(getSmsAuthService().getExpiresAt(session))
|
||||
.resendableAt(getSmsAuthService().getResendableAt(session))
|
||||
.smsTestHint(getSmsAuthService().getFixedModeHintText())
|
||||
.build();
|
||||
} else {
|
||||
return SmsAuthDto.builder()
|
||||
@@ -119,6 +121,7 @@ public class SmsAuthController implements InterceptorSkipController {
|
||||
.maskedPhone(getSmsAuthService().maskPhoneNumber(userInfo.getCphnno()))
|
||||
.expiresAt(getSmsAuthService().getExpiresAt(session))
|
||||
.resendableAt(getSmsAuthService().getResendableAt(session))
|
||||
.smsTestHint(getSmsAuthService().getFixedModeHintText())
|
||||
.build();
|
||||
} else {
|
||||
return SmsAuthDto.builder()
|
||||
@@ -208,6 +211,45 @@ public class SmsAuthController implements InterceptorSkipController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SMS 인증번호 검증 및 비밀번호 변경 완료
|
||||
*/
|
||||
@RequestMapping(value = "/verify-password-change.json", method = RequestMethod.POST)
|
||||
@ResponseBody
|
||||
public ChangePasswordResponseDto verifyPasswordChangeAuthCode(HttpServletRequest request,
|
||||
@RequestParam("authCode") String authCode) {
|
||||
HttpSession session = request.getSession();
|
||||
UserInfo userInfo = getSmsAuthService().getUserInfoFromSession(session);
|
||||
String pendingPasswordHash = getSmsAuthService().getPendingPasswordHashFromSession(session);
|
||||
|
||||
if (userInfo == null || pendingPasswordHash == null) {
|
||||
log.warn("SMS 인증 세션 정보 없음 - verify-password-change 요청");
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message("인증 세션이 만료되었습니다. 다시 시도해주세요.")
|
||||
.build();
|
||||
}
|
||||
|
||||
if (getSmsAuthService().isExpired(session)) {
|
||||
log.warn("SMS 인증번호 만료됨(비밀번호 변경) - userId: {}", userInfo.getUserid());
|
||||
getSmsAuthService().clearAuthSession(session);
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message("인증번호가 만료되었습니다. 다시 시도해주세요.")
|
||||
.build();
|
||||
}
|
||||
|
||||
if (!getSmsAuthService().validateAuthCode(session, authCode)) {
|
||||
log.warn("SMS 인증번호 불일치(비밀번호 변경) - userId: {}", userInfo.getUserid());
|
||||
return ChangePasswordResponseDto.builder()
|
||||
.success(false)
|
||||
.message("인증번호가 일치하지 않습니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
return mainController.commitPendingPasswordChange(request, session);
|
||||
}
|
||||
|
||||
/**
|
||||
* SMS 인증 상태 조회
|
||||
*/
|
||||
|
||||
@@ -50,4 +50,9 @@ public class SmsAuthDto {
|
||||
* 에러 메시지
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* djb.sms_auth.mode=fixed 일 때만 채워지는 테스트용 인증번호 안내 문구 (예: "[테스트] 인증번호: 000000")
|
||||
*/
|
||||
private String smsTestHint;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ public class SmsAuthService {
|
||||
public static final String SESSION_KEY_ISSUED_AT = "SMS_AUTH_ISSUED_AT";
|
||||
public static final String SESSION_KEY_USER_INFO = "SMS_AUTH_USER_INFO";
|
||||
public static final String SESSION_KEY_LOGIN_VO = "SMS_AUTH_LOGIN_VO";
|
||||
public static final String SESSION_KEY_PENDING_PASSWORD_HASH = "SMS_AUTH_PENDING_PASSWORD_HASH";
|
||||
|
||||
// 시간 설정 (밀리초)
|
||||
public static final long EXPIRY_MS = 60_000; // 1분
|
||||
@@ -130,6 +131,27 @@ public class SmsAuthService {
|
||||
log.debug("SMS 인증 세션 설정 완료 - userId: {}", userInfo.getUserid());
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경용 SMS 인증 세션 설정.
|
||||
* 로그인 플로우의 {@link #setupAuthSession}과 세션 키(AUTH_CODE/ISSUED_AT/USER_INFO)를 공유하되,
|
||||
* LOGIN_VO 대신 검증 성공 시 실제로 반영할 새 비밀번호 해시를 저장한다.
|
||||
*/
|
||||
public void setupPasswordChangeAuthSession(HttpSession session, UserInfo userInfo, String pendingPasswordHash, String authCode) {
|
||||
long now = System.currentTimeMillis();
|
||||
session.setAttribute(SESSION_KEY_AUTH_CODE, authCode);
|
||||
session.setAttribute(SESSION_KEY_ISSUED_AT, now);
|
||||
session.setAttribute(SESSION_KEY_USER_INFO, userInfo);
|
||||
session.setAttribute(SESSION_KEY_PENDING_PASSWORD_HASH, pendingPasswordHash);
|
||||
log.debug("SMS 인증 세션(비밀번호 변경용) 설정 완료 - userId: {}", userInfo.getUserid());
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션에서 대기 중인 새 비밀번호 해시 조회 (비밀번호 변경 SMS 인증용)
|
||||
*/
|
||||
public String getPendingPasswordHashFromSession(HttpSession session) {
|
||||
return (String) session.getAttribute(SESSION_KEY_PENDING_PASSWORD_HASH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증번호 검증
|
||||
*/
|
||||
@@ -224,6 +246,7 @@ public class SmsAuthService {
|
||||
session.removeAttribute(SESSION_KEY_ISSUED_AT);
|
||||
session.removeAttribute(SESSION_KEY_USER_INFO);
|
||||
session.removeAttribute(SESSION_KEY_LOGIN_VO);
|
||||
session.removeAttribute(SESSION_KEY_PENDING_PASSWORD_HASH);
|
||||
log.debug("SMS 인증 세션 정보 클리어 완료");
|
||||
}
|
||||
|
||||
@@ -266,4 +289,17 @@ public class SmsAuthService {
|
||||
public boolean isEnabled() {
|
||||
return getProperty().isSmsAuthEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* fixed 모드(djb.sms_auth.mode=fixed)일 때 화면에 표시할 테스트용 안내 문구.
|
||||
* 실제 SMS 발송 없이도 개발/테스트 환경에서 인증번호를 알 수 있도록 노출한다.
|
||||
* fixed 모드가 아니면 null (화면에서는 힌트를 표시하지 않음).
|
||||
*/
|
||||
public String getFixedModeHintText() {
|
||||
DjbProperty prop = getProperty();
|
||||
if (!MODE_FIXED.equalsIgnoreCase(prop.getSmsAuthMode())) {
|
||||
return null;
|
||||
}
|
||||
return "[테스트] 인증번호: " + prop.getSmsAuthFixedValue();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user