45 Commits

Author SHA1 Message Date
eastargh 1aa40f2c61 UMS관련 정리 2026-06-09 15:39:05 +09:00
eastargh c8caeeda8c 유량제어 모니터링 2026-06-09 13:04:32 +09:00
eastargh 06ad1230b2 유량제어 모니터링 2026-06-09 11:34:10 +09:00
eastargh 8109f91aa2 유량제어 현황 모니터링 기능 추가 2026-06-09 09:51:05 +09:00
eastargh 62ccebd773 Q&A 7일후 자동 종료 배치 2026-06-08 17:19:04 +09:00
eastargh ec8d6bfd7b Q&A 게시판 댓글 작성 기능 2026-06-05 17:41:37 +09:00
eastargh 480d7dac5a 앱승인시 예상완료일 항목 추가 & 승인요청 상세 조회시 "진행중"으로 상태변경 2026-06-05 15:10:06 +09:00
eastargh feb4f845a8 팝업 두번째 이후 오픈시 선택버그 수정 2026-06-05 10:11:21 +09:00
eastargh 7857f5b45c 자동 Log-off 기능 추가 2026-06-04 18:11:31 +09:00
eastargh 68c8e18e8d 스윙채 & webhook 발송 수정 2026-06-04 17:38:52 +09:00
eastargh 35e5564252 webhook 발송 2026-06-02 09:43:03 +09:00
eastargh 0d7d006f92 api 상태 변화 감지 & 웹훅 발송 2026-06-01 17:37:06 +09:00
eastargh 737b493f7c API사용현황 통계 2026-05-28 16:02:52 +09:00
eastargh af0a878a39 DB스키마변경 BAPADM->BAPAPP 2026-05-28 11:32:31 +09:00
eastargh b639e29039 DB스키마 변경 : AGWADM->AGWAPP, EMSADM->EMSAPP 2026-05-28 10:53:33 +09:00
eastargh 350297aaa2 클라이언트 유량제어 테이블 수정 2026-05-27 16:54:19 +09:00
eastargh 7677e3b26e 거래로그 집계시 공통코드 테이블 변경 2026-05-21 15:10:24 +09:00
eastargh 191857811f 공지사항 첨부파일 안씀 2026-05-20 11:50:40 +09:00
eastargh 88ffc1cc28 API 사용현황 신규개발 2026-05-20 11:45:00 +09:00
eastargh d3bd4acb62 요청/응답구분 선택 버그 수정 2026-05-19 10:57:48 +09:00
eastargh 1f7166502f API통계 업무오류 제외 2026-05-15 14:53:52 +09:00
eastargh 91f36605e8 스케쥴러 실행시간 format 변경 2026-05-14 11:22:47 +09:00
eastargh 0ebd31e256 API 통계 화면 리뉴얼 2026-05-14 11:21:14 +09:00
eastargh 45c6edbbeb 거래로그>시간별 수동집계 기능 추가 2026-05-14 11:20:33 +09:00
eastargh 335c7ec53a 거래로그 > 시간별 집계하는 배치잡 2026-05-14 11:19:55 +09:00
eastargh 0b341b85f1 평균응답속도 계산시 분모를 성공건수에서 총건수로 변경 2026-05-14 11:17:55 +09:00
eastargh e9ac62d3ed 인터페이스 상세 저장 버그 수정 2026-05-12 10:37:32 +09:00
eastargh 133c05f1d7 알림 발송 관련 배치 리네이밍 2026-05-08 17:24:17 +09:00
eastargh 4a1a4be54d 검색조건 유지 2026-05-08 10:37:57 +09:00
curry772 dbd36f28d4 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin 2026-05-07 15:39:27 +09:00
curry772 4ee50be000 Merge branch 'feature/crypto-module' into master 2026-05-07 15:39:13 +09:00
curry772 3d916eda53 feat: 암호화 모듈 저장/삭제 후 online 서버 broadcast 결과 alert 출력
- CryptoModuleConfigManController: INSERT/UPDATE/DELETE 모두 broadcast()
  결과(CommandResult)를 broadcastResult 필드로 응답에 포함
- cryptoModuleManDetail.jsp: 저장/삭제 success 콜백에서 broadcastResult를
  alert 메시지에 추가 출력 (서버명=결과 형태)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 15:35:46 +09:00
curry772 49fdc3b577 암호화 모듈 설정 관리 화면 추가 (feature/crypto-module)
- CryptoModuleConfig CRUD 관리 화면 신규 개발
  - JPA DataService/Repository, MapStruct UiMapper, ManService, Controller
  - 목록(jqGrid, 4가지 검색조건), 상세/등록/수정/삭제 화면
  - INSERT/UPDATE/DELETE 후 게이트웨이 전 인스턴스에 ReloadCryptoModuleCommand broadcast
- 버그 수정
  - getById(getReferenceById) → findById 교체: Persistable.isNew=true 유지로 인한 persist 에러
  - insert 시 entity.setCryptoId(null): 빈 문자열 cryptoId 전송 시 UUID 생성기 오동작 방지
  - 검색 버튼 클릭 시 algType/keySourceType/useYn getSearchForJqgrid 누락 수정
- UX 개선
  - 암호화키/복호화키/IV 필드에 텍스트→HEX 변환 보조 입력 및 키 길이 실시간 표시
  - 상세 화면에 수정자/수정일시 정보성 표시
- DB: EMSADM.CRYPTO_MODULE_CONFIG 테이블 (MODIFIED_BY/MODIFIED_AT 포함 전체 DDL)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 10:51:39 +09:00
eastargh 9d55db3281 게시판관리 - 게시유형, 고정여부 추가 2026-05-07 09:50:49 +09:00
eastargh 9a5d5e4815 클라이언트 유량제어 신규생성 2026-05-07 09:39:59 +09:00
eastargh e9bff78cbf 웹훅 발송/수신 sample 2026-04-30 17:44:52 +09:00
eastargh 6ccd669ec6 유량제어 토큰 획득 실패 감시 JOB 2026-04-30 17:44:06 +09:00
eastargh 5db046eb10 API 상태 변화 감시 job 2026-04-30 17:42:20 +09:00
eastargh 8bd8558038 API수정시 삭제버그 수정 2026-04-30 17:14:08 +09:00
eastargh c3f5d6a42e orderSeq 정렬 추가 2026-04-30 17:12:02 +09:00
eastargh 72cdb04261 이중로그인 프라퍼티 적용 & 이중로그인 로그아웃 버그 수정 2026-04-29 15:36:25 +09:00
curry772 d3601c3991 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin 2026-04-22 14:42:19 +09:00
eastargh e213c96994 인터페이스 헤더 어댑터 REST PATH 추가가능 2026-04-16 11:00:26 +09:00
curry772 889d1fcfad BAP JDBC 설정 제거 2026-04-10 17:17:53 +09:00
curry772 45f980f638 netty 라이브러리 버젼 통일 2026-04-10 17:17:21 +09:00
138 changed files with 7176 additions and 1485 deletions
-2
View File
@@ -1,5 +1,3 @@
gradle.properties
# Package Files # # Package Files #
*.war *.war
@@ -25,7 +25,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
@@ -37,7 +37,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP" p:name="BAP"
p:text="BAP" p:text="BAP"
p:schema="BAPADM" p:schema="BAPAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_BAP" p:jndiName="jdbc/dsOBP_BAP"
@@ -49,7 +49,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -25,31 +25,31 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
p:render="ONL,COM" p:render="ONL,COM"
/> />
<!-- BAP 일괄전송FTP --> <!-- BAP 일괄전송FTP -->
<!-- <bean--> <bean
<!-- id="BAP"--> id="BAP"
<!-- class="com.eactive.eai.rms.common.datasource.DataSourceType"--> class="com.eactive.eai.rms.common.datasource.DataSourceType"
<!-- p:name="BAP"--> p:name="BAP"
<!-- p:text="BAP"--> p:text="BAP"
<!-- p:schema="BAPADM"--> p:schema="BAPAPP"
<!-- p:dynamic="true"--> p:dynamic="true"
<!-- p:online="true"--> p:online="true"
<!-- p:jndiName="jdbc/dsOBP_BAP"--> p:jndiName="jdbc/dsOBP_BAP"
<!-- p:render="BAP,COM"--> p:render="BAP,COM"
<!-- />--> />
<!-- RMS_DEFAULT --> <!-- RMS_DEFAULT -->
<bean <bean
id="MONITORING" id="MONITORING"
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -25,7 +25,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
@@ -37,7 +37,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP" p:name="BAP"
p:text="BAP" p:text="BAP"
p:schema="BAPADM" p:schema="BAPAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_BAP" p:jndiName="jdbc/dsOBP_BAP"
@@ -49,7 +49,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -25,7 +25,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
@@ -37,7 +37,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP" p:name="BAP"
p:text="BAP" p:text="BAP"
p:schema="BAPADM" p:schema="BAPAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_BAP" p:jndiName="jdbc/dsOBP_BAP"
@@ -49,7 +49,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -30,9 +30,9 @@
<entry <entry
key="APIGW" key="APIGW"
value-ref="APIGW" /> value-ref="APIGW" />
<!-- <entry--> <entry
<!-- key="BAP"--> key="BAP"
<!-- value-ref="BAP" />--> value-ref="BAP" />
</map> </map>
</property> </property>
<property <property
@@ -91,7 +91,7 @@
<property name="configLocations"> <property name="configLocations">
<list> <list>
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfOnl.xml</value> <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfOnl.xml</value>
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value>--> <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value>
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBat.xml</value> <!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBat.xml</value>
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfService.xml</value> --> <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfService.xml</value> -->
</list> </list>
@@ -69,6 +69,7 @@
<context:component-scan base-package="com.eactive.apim.portal" /> <context:component-scan base-package="com.eactive.apim.portal" />
<context:component-scan base-package="com.eactive.ext.kjb.statistics" /> <context:component-scan base-package="com.eactive.ext.kjb.statistics" />
<context:component-scan base-package="com.eactive.ext.djb" />
<bean id="cacheManager" <bean id="cacheManager"
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr11 inst.Name=emsSvr11
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=D eai.systemmode=D
eai.drmode=N eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr96 inst.Name=emsSvr96
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=L eai.systemmode=L
eai.drmode=N eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr11 inst.Name=emsSvr11
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=P eai.systemmode=P
eai.drmode=N eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr11 inst.Name=emsSvr11
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=T eai.systemmode=T
eai.drmode=N eai.drmode=N
+12 -9
View File
@@ -9,15 +9,7 @@
<param-name>contextConfigLocation</param-name> <param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value> <param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param> </context-param>
<!-- XSS Filter -->
<filter>
<filter-name>CrossScriptingFilter</filter-name>
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CrossScriptingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter> <filter>
<filter-name>encodingFilter</filter-name> <filter-name>encodingFilter</filter-name>
@@ -57,6 +49,17 @@
<url-pattern>*.json</url-pattern> <url-pattern>*.json</url-pattern>
</filter-mapping> </filter-mapping>
<!-- XSS Filter -->
<filter>
<filter-name>CrossScriptingFilter</filter-name>
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CrossScriptingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- <!--
<filter> <filter>
<filter-name>MonitoringFrontFilter</filter-name> <filter-name>MonitoringFrontFilter</filter-name>
+8
View File
@@ -36,6 +36,14 @@
// save the returnValue in options so that it is available in the callback function // save the returnValue in options so that it is available in the callback function
optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.returnValue = $frame[0].contentWindow.window.returnValue;
optns.onClose(); optns.onClose();
// Remove iframe from DOM so window.frames doesn't accumulate stale entries;
// without this, a second showModal call leaves frames[0] pointing to the first
// (closed) iframe, causing the previous returnValue to bleed into the next callback.
var $dialogEl = $frame.closest('.ui-dialog');
setTimeout(function() {
$frame.remove();
if ($dialogEl.length) $dialogEl.remove();
}, 0);
if(optns.option && optns.option.fullScreenPopup){ if(optns.option && optns.option.fullScreenPopup){
$('iframe[name=topFrame]',window.parent.document).css('display', 'block'); $('iframe[name=topFrame]',window.parent.document).css('display', 'block');
+4
View File
@@ -30,6 +30,10 @@
<script language="javascript" src="<c:url value="/js/jquery.contextMenu.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.contextMenu.js"/>"></script>
<script language="javascript" src="<c:url value="/js/custom.js"/>"></script> <script language="javascript" src="<c:url value="/js/custom.js"/>"></script>
<script language="javascript" src="<c:url value="/js/common-${locale}.js"/>"></script> <script language="javascript" src="<c:url value="/js/common-${locale}.js"/>"></script>
<script type="text/javascript">
/* 자동 로그아웃: iframe 내 활동을 sessionStorage에 기록 (main.jsp 타이머와 공유) */
(function() { var m = function() { sessionStorage.setItem('lastActivity', String(Date.now())); }; ['mousemove','keydown','click','touchstart'].forEach(function(e){ document.addEventListener(e, m, true); }); })();
</script>
<script language="javascript" src="<c:url value="/js/jquery.inputmask.bundle.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.inputmask.bundle.min.js"/>"></script>
<script language="javascript" src="<c:url value="/js/jquery.searchabledropdown-1.0.8.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.searchabledropdown-1.0.8.min.js"/>"></script>
<%-- <script language="javascript" src="<c:url value="/js/jquery.searchable-1.1.0.min.js"/>"></script> --%> <%-- <script language="javascript" src="<c:url value="/js/jquery.searchable-1.1.0.min.js"/>"></script> --%>
@@ -31,19 +31,19 @@ function init(){
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(); "<%= localeMessage.getString("combo.all")%>").setData(json.instanceList).rendering();
if(typeof callback === 'function') { putSelectFromParam();
callback(url,key);
} initGrid();
}, },
error:function(e){ error:function(e){
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
$(document).ready(function() {
init(); function initGrid() {
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
console.log(gridPostData);
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
@@ -125,8 +125,12 @@ $(document).ready(function() {
} }
}); });
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
}
$(document).ready(function() {
init();
$("#btn_search").click(function(){ $("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST"); var postData = getSearchForJqgrid("cmd","LIST");
@@ -465,21 +465,21 @@ $(document).ready(function() {
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<tr> <tr>
<th style="width:180px;">Default Scheduler</th> <th style="width:180px;">Default Scheduler</th>
<td>On Memory: <span id="deft_onMemory"></span></td> <td style="width:150px">On Memory: <span id="deft_onMemory"></span></td>
<td style="width: 260px;">Previous Fired: <span id="deft_previous"></span></td> <td >Previous Fired: <span id="deft_previous"></span></td>
<td style="width: 260px;">Next Fire Time: <span id="deft_next"></span></td> <td >Next Fire Time: <span id="deft_next"></span></td>
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td> <td style="width: 250px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td>
<td style="width: 200px;">PersistJobData: <span id="deft_persistJobData"></span></td> <td style="width: 120px;">PersistJobData: <span id="deft_persistJobData"></span></td>
<td style="width: 200px;">Durable: <span id="deft_durable"></span></td> <td style="width: 120px;">Durable: <span id="deft_durable"></span></td>
</tr> </tr>
<tr> <tr>
<th style="width:180px;">Clustered Scheduler</th> <th style="width:180px;">Clustered Scheduler</th>
<td>On Memory: <span id="clus_onMemory"></span></td> <td style="width:150px">On Memory: <span id="clus_onMemory"></span></td>
<td style="width: 260px;">Previous Fired: <span id="clus_previous"></span></td> <td>Previous Fired: <span id="clus_previous"></span></td>
<td style="width: 260px;">Next Fire Time: <span id="clus_next"></span></td> <td>Next Fire Time: <span id="clus_next"></span></td>
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td> <td style="width: 250px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td>
<td style="width: 200px;">PersistJobData: <span id="clus_persistJobData"></span></td> <td style="width: 120px;">PersistJobData: <span id="clus_persistJobData"></span></td>
<td style="width: 200px;">Durable: <span id="clus_durable"></span></td> <td style="width: 120px;">Durable: <span id="clus_durable"></span></td>
</tr> </tr>
</table> </table>
+87
View File
@@ -99,6 +99,93 @@
<iframe class="topMenu" src="" id="topFrame" name="topFrame" scrolling="no" noresize="noresize"></iframe> <iframe class="topMenu" src="" id="topFrame" name="topFrame" scrolling="no" noresize="noresize"></iframe>
<iframe class="leftMenu" src="" name="leftFrame" scrolling="no" noresize="noresize"></iframe> <iframe class="leftMenu" src="" name="leftFrame" scrolling="no" noresize="noresize"></iframe>
<iframe class="rightCon" src="" name="mainFrame" scrolling="auto"></iframe> <iframe class="rightCon" src="" name="mainFrame" scrolling="auto"></iframe>
<%-- 자동 로그아웃 경고 모달 --%>
<div id="autoLogoutModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:99999;">
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; padding:30px 40px; border-radius:4px; text-align:center; min-width:320px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<h3 style="margin:0 0 12px; color:#333; font-size:16px;">자동 로그아웃 안내</h3>
<p style="margin:0 0 22px; color:#666; font-size:14px;">
장시간 사용하지 않아 <strong><span id="autoLogoutSeconds">60</span>초</strong> 후 자동 로그아웃됩니다.
</p>
<button id="autoLogoutContinue" style="margin-right:8px; padding:8px 22px; background:#337ab7; color:#fff; border:none; border-radius:3px; cursor:pointer; font-size:13px;">계속 사용</button>
<button id="autoLogoutNow" style="padding:8px 22px; background:#d9534f; color:#fff; border:none; border-radius:3px; cursor:pointer; font-size:13px;">로그아웃</button>
</div>
</div>
<script type="text/javascript">
(function () {
var IDLE_MS = ${autoLogoutTimeout} * 60 * 1000; // 설정값(분) → ms
var WARN_MS = 60 * 1000; // 로그아웃 1분 전 경고
var CHECK_INTERVAL = 5000; // 5초 간격 체크
var countdownTimer = null;
/* sessionStorage: iframe 포함 모든 동일-origin 프레임과 공유 */
function getLastActivity() {
var t = sessionStorage.getItem('lastActivity');
return t ? parseInt(t, 10) : Date.now();
}
function markActivity() {
sessionStorage.setItem('lastActivity', String(Date.now()));
}
function resetTimer() {
markActivity();
if ($('#autoLogoutModal').is(':visible')) {
$('#autoLogoutModal').hide();
clearInterval(countdownTimer);
}
}
function showWarning(remainMs) {
var sec = Math.ceil(remainMs / 1000);
$('#autoLogoutSeconds').text(sec);
$('#autoLogoutModal').show();
clearInterval(countdownTimer);
countdownTimer = setInterval(function () {
sec--;
if (sec <= 0) {
clearInterval(countdownTimer);
doLogout();
} else {
$('#autoLogoutSeconds').text(sec);
}
}, 1000);
}
function doLogout() {
location.href = '<%=request.getContextPath()%>/rms/logout.do';
}
/* 상위 프레임(main.jsp) 활동 감지 */
['mousemove', 'keydown', 'click', 'touchstart'].forEach(function (e) {
document.addEventListener(e, markActivity, true);
});
/* 주기적 유휴 시간 체크 */
setInterval(function () {
var idle = Date.now() - getLastActivity();
var remain = IDLE_MS - idle;
if (remain <= 0) {
clearInterval(countdownTimer);
doLogout();
} else if (remain <= WARN_MS && !$('#autoLogoutModal').is(':visible')) {
showWarning(remain);
}
}, CHECK_INTERVAL);
/* 버튼 이벤트 */
$(document).on('click', '#autoLogoutContinue', function () {
resetTimer();
$.get('<%=request.getContextPath()%>/checkSession.do'); // 서버 세션도 갱신
});
$(document).on('click', '#autoLogoutNow', function () {
doLogout();
});
/* 페이지 로드 시 초기화 */
markActivity();
})();
</script>
</body> </body>
</html> </html>
+8
View File
@@ -36,6 +36,14 @@
// save the returnValue in options so that it is available in the callback function // save the returnValue in options so that it is available in the callback function
optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.returnValue = $frame[0].contentWindow.window.returnValue;
optns.onClose(); optns.onClose();
// Remove iframe from DOM so window.frames doesn't accumulate stale entries;
// without this, a second showModal call leaves frames[0] pointing to the first
// (closed) iframe, causing the previous returnValue to bleed into the next callback.
var $dialogEl = $frame.closest('.ui-dialog');
setTimeout(function() {
$frame.remove();
if ($dialogEl.length) $dialogEl.remove();
}, 0);
if(optns.option && optns.option.fullScreenPopup){ if(optns.option && optns.option.fullScreenPopup){
$('iframe[name=topFrame]',window.parent.document).css('display', 'block'); $('iframe[name=topFrame]',window.parent.document).css('display', 'block');
@@ -16,10 +16,39 @@
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript"> <script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />';
var isDetail = false; var isDetail = false;
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) { function init(url, key, callback) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
@@ -29,6 +58,7 @@ function init(url,key,callback){
success: function(json) { success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(url, key); callback(url, key);
@@ -39,6 +69,7 @@ function init(url,key,callback){
} }
}); });
} }
function detail(url, key) { function detail(url, key) {
if (!isDetail) return; if (!isDetail) return;
$.ajax({ $.ajax({
@@ -57,12 +88,181 @@ function detail(url,key){
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
}); });
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
}, },
error: function(e) { error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', adapterId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '어댑터가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() { $(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key = "${param.name}"; var key = "${param.name}";
@@ -71,31 +271,51 @@ $(document).ready(function() {
} }
init(url, key, detail); init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() { $("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 어댑터를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
if (isDetail) { if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"}); postData.push({name: "cmd", value: "UPDATE"});
} else { } else {
postData.push({name: "cmd", value: "INSERT"}); postData.push({name: "cmd", value: "INSERT"});
} }
var isInsert = !isDetail;
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: url, url: url,
data: postData, data: postData,
success: function(args) { success: function(args) {
alert("<%= localeMessage.getString("common.saveMsg") %>"); showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
goNav(returnUrl);//LIST로 이동 type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
}, },
error: function(e) { error: function(e) {
alert(e.responseText); showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_delete").click(function() { $("#btn_delete").click(function() {
$(this).blur();
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return; var adapterName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + adapterName + ']</strong> 어댑터를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"}); postData.push({name: "cmd", value: "DELETE"});
$.ajax({ $.ajax({
@@ -103,23 +323,39 @@ $(document).ready(function() {
url: url, url: url,
data: postData, data: postData,
success: function(args) { success: function(args) {
alert("<%= localeMessage.getString("common.deleteMsg") %>"); showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
goNav(returnUrl);//LIST로 이동 type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
}, },
error: function(e) { error: function(e) {
alert(e.responseText); showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_previous").click(function() { $("#btn_previous").click(function() {
goNav(returnUrl);//LIST로 이동 goNav(returnUrl);
}); });
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
}); });
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</script> </script>
</head> </head>
<body> <body>
@@ -129,60 +365,137 @@ $(document).ready(function() {
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button> <button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button> <button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button> <button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div> </div>
<div class="title-wrap">
<div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip"><%= localeMessage.getString("infAdpConMan.title") %></span></div> <div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip"><%= localeMessage.getString("infAdpConMan.title") %></span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<table id="grid" ></table> <!-- 탭 네비게이션 -->
<div id="pager"></div> <div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- detail --> <!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm"> <form id="ajaxForm">
<table class="table_row" cellspacing="0"> <span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th> <th><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<td><input type="text" name="name" /></td> <td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaDes") %></th> <th><%= localeMessage.getString("infAdpConMan.adaDes") %></th>
<td><input type="text" name="desc" /></td> <td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th> <th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td> <td>
<div class="select-style"> <select name="useYn" class="styled-select"></select>
<select name="thresholdTimeUnit" /> </td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th> <th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td> <td>
<div class="select-style"> <div class="threshold-group">
<select name="useYn" /> <select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div> </div>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</form> </form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle --> </div><!-- end content_middle -->
</div><!-- end right_box --> </div><!-- end right_box -->
</body> </body>
</html> </html>
@@ -0,0 +1,138 @@
<%@ 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/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
$(document).ready(function() {
$('#grid').jqGrid({
datatype:"json",
mtype: 'POST',
url: url,
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()},
colNames:['클라이언트 ID',
'클라이언트명',
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
'<%= localeMessage.getString("infAdpConMan.thr") %>',
'<%= localeMessage.getString("infAdpConMan.thrTimeUnit") %>',
'<%= localeMessage.getString("infAdpConMan.useYn") %>'
],
colModel:[
{ name : 'NAME' , align:'left' , width:'100' , sortable:false },
{ name : 'DESC' , align:'left' , width:'200'},
{ name : 'THRESHOLDPERSECOND' , align:'right' , width:'60'},
{ name : 'THRESHOLD' , align:'right' , width:'60'},
{ name : 'THRESHOLDTIMEUNIT' , align:'center' , width:'80'},
{ name : 'USEYN' , align:'center' , width:'60' , editoptions:{value:"0:사용안함;1:사용함"}, formatter:"select" }
],
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 name = rowData['NAME'];
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$(this).getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchName='+$("input[name=searchName]").val();
//key값
url2 += '&name='+name;
goNav(url2);
},
gridComplete:function (d){
var colModel = $(this).getGridParam("colModel");
for(var i = 0 ; i< colModel.length; i++){
$(this).setColProp(colModel[i].name, {sortable : false});
}
}
});
resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid");
});
$("#btn_new").click(function(){
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$("#grid").getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchName=';
goNav(url2);
});
$("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">클라이언트(API Key) 유량제어를 관리한다</span></div>
<table class="search_condition" cellspacing=0;>
<tbody>
<tr>
<th style="width:180px;">클라이언트명</th>
<td><input type="text" name="searchName" value="${param.searchName}"></td>
</tr>
</tbody>
</table>
<table id="grid" ></table>
<div id="pager"></div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -0,0 +1,516 @@
<%@ 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"/>
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
var isDetail = false;
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if (typeof callback === 'function') {
callback(url, key);
}
},
error: function(e) {
alert(e.responseText);
}
});
}
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
});
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
},
error: function(e) {
alert(e.responseText);
}
});
}
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
//버킷 현황 조회
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', clientId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
// 전체 합산 계산
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
// 요약 영역 표시
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '그룹이 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 클라이언트를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"});
} else {
postData.push({name: "cmd", value: "INSERT"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_delete").click(function() {
$(this).blur();
var clientName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + clientName + ']</strong> 클라이언트를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_previous").click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
$(document).keydown(function(e) {
if (e.keyCode === 27) {
// ESC 키 처리 (필요시 모달 닫기 등)
}
});
});
</script>
</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_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title-wrap">
<div class="title">클라이언트 유량제어<span class="tooltip">클라이언트 유량제어</span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- 탭 네비게이션 -->
<div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm">
<span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>클라이언트 ID</th>
<td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th>클라이언트명</th>
<td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<select name="useYn" class="styled-select"></select>
</td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
<div class="bucket-summary-item">
<span class="bucket-summary-label">적용 API</span>
<span class="bucket-summary-value" id="summaryActiveApi">0<span class="unit">개</span></span>
<span class="bucket-summary-sub" id="summaryActiveApiList"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -16,10 +16,39 @@
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript"> <script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />';
var isDetail = false; var isDetail = false;
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) { function init(url, key, callback) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
@@ -29,6 +58,7 @@ function init(url,key,callback){
success: function(json) { success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if (typeof callback === 'function') { if (typeof callback === 'function') {
callback(url, key); callback(url, key);
@@ -39,6 +69,7 @@ function init(url,key,callback){
} }
}); });
} }
function detail(url, key) { function detail(url, key) {
if (!isDetail) return; if (!isDetail) return;
$.ajax({ $.ajax({
@@ -57,12 +88,181 @@ function detail(url,key){
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
}); });
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
}, },
error: function(e) { error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', interfaceId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '인터페이스가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() { $(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key = "${param.name}"; var key = "${param.name}";
@@ -71,31 +271,51 @@ $(document).ready(function() {
} }
init(url, key, detail); init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() { $("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 인터페이스를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
if (isDetail) { if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"}); postData.push({name: "cmd", value: "UPDATE"});
} else { } else {
postData.push({name: "cmd", value: "INSERT"}); postData.push({name: "cmd", value: "INSERT"});
} }
var isInsert = !isDetail;
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: url, url: url,
data: postData, data: postData,
success: function(args) { success: function(args) {
alert("<%= localeMessage.getString("common.saveMsg") %>"); showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
goNav(returnUrl);//LIST로 이동 type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
}, },
error: function(e) { error: function(e) {
alert(e.responseText); showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_delete").click(function() { $("#btn_delete").click(function() {
$(this).blur();
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return; var ifName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + ifName + ']</strong> 인터페이스를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"}); postData.push({name: "cmd", value: "DELETE"});
$.ajax({ $.ajax({
@@ -103,23 +323,39 @@ $(document).ready(function() {
url: url, url: url,
data: postData, data: postData,
success: function(args) { success: function(args) {
alert("<%= localeMessage.getString("common.deleteMsg") %>"); showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
goNav(returnUrl);//LIST로 이동 type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
}, },
error: function(e) { error: function(e) {
alert(e.responseText); showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_previous").click(function() { $("#btn_previous").click(function() {
goNav(returnUrl);//LIST로 이동 goNav(returnUrl);
}); });
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
}); });
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</script> </script>
</head> </head>
<body> <body>
@@ -129,60 +365,137 @@ $(document).ready(function() {
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button> <button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button> <button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button> <button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div> </div>
<div class="title-wrap">
<div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip"><%= localeMessage.getString("infIfConMan.title") %></span></div> <div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip"><%= localeMessage.getString("infIfConMan.title") %></span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<table id="grid" ></table> <!-- 탭 네비게이션 -->
<div id="pager"></div> <div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- detail --> <!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm"> <form id="ajaxForm">
<table class="table_row" cellspacing="0"> <span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifNm") %></th> <th><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<td><input type="text" name="name" /></td> <td><input type="text" name="name" style="width:300px;background-color: #ebebec;" /></td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifDesc") %></th> <th><%= localeMessage.getString("infIfConMan.ifDesc") %></th>
<td><input type="text" name="desc" /></td> <td><input type="text" name="desc" style="width:300px;background-color: #ebebec;" /></td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th> <th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td> <td>
<div class="select-style"> <select name="useYn" class="styled-select"></select>
<select name="thresholdTimeUnit" /> </td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th> <th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td> <td>
<div class="select-style"> <div class="threshold-group">
<select name="useYn" /> <select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div> </div>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</form> </form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle --> </div><!-- end content_middle -->
</div><!-- end right_box --> </div><!-- end right_box -->
</body> </body>
</html> </html>
@@ -0,0 +1,153 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
$(document).ready(function() {
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
url: url,
postData: {
cmd: 'LIST',
searchName: $('input[name=searchName]').val(),
algType: $('select[name=algType]').val(),
keySourceType: $('select[name=keySourceType]').val(),
useYn: $('select[name=useYn]').val()
},
colNames: ['ID', '모듈명', '설명', '알고리즘', '운영모드', '키 소스', '사용'],
colModel: [
{ name: 'CRYPTO_ID', hidden: true },
{ name: 'CRYPTO_NAME', align: 'left' },
{ name: 'CRYPTO_DESC', align: 'left', sortable: false },
{ name: 'ALG_TYPE', align: 'center', width: 80 },
{ name: 'CIPHER_MODE', align: 'center', width: 80 },
{ name: 'KEY_SOURCE_TYPE', align: 'center', width: 100 },
{ name: 'USE_YN', align: 'center', width: 60 }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var cryptoId = rowData['CRYPTO_ID'];
var url2 = url_view + '?cmd=DETAIL';
url2 += '&page=' + $(this).getGridParam("page");
url2 += '&returnUrl=' + getReturnUrl();
url2 += '&menuId=' + '${param.menuId}';
url2 += '&searchName=' + $("input[name=searchName]").val();
url2 += '&cryptoId=' + cryptoId;
goNav(url2);
},
gridComplete: function() {
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false });
}
},
loadError: function(jqXHR, textStatus, errorThrown) {
var location = '<%=request.getContextPath()%>/';
comloadError(jqXHR, textStatus, errorThrown, location);
}
});
resizeJqGridWidth('grid', 'content_middle', '1000');
$("#btn_search").click(function() {
var postData = getSearchForJqgrid("cmd", "LIST");
postData['algType'] = $('select[name=algType]').val();
postData['keySourceType'] = $('select[name=keySourceType]').val();
postData['useYn'] = $('select[name=useYn]').val();
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
});
$("#btn_new").click(function() {
var url2 = url_view + '?cmd=DETAIL';
url2 += '&page=' + $("#grid").getGridParam("page");
url2 += '&returnUrl=' + getReturnUrl();
url2 += '&menuId=' + '${param.menuId}';
goNav(url2);
});
$("input[name=searchName]").keydown(function(key) {
if (key.keyCode == 13) { $("#btn_search").click(); }
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> 신규</button>
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> 검색</button>
</div>
<div class="title">암호화 모듈 설정</div>
<form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
<th style="width:100px;">모듈명</th>
<td><input type="text" name="searchName" value="${param.searchName}"></td>
<th style="width:100px;">알고리즘</th>
<td>
<select name="algType">
<option value="">전체</option>
<option value="AES">AES</option>
<option value="ARIA">ARIA</option>
</select>
</td>
<th style="width:100px;">키 소스</th>
<td>
<select name="keySourceType">
<option value="">전체</option>
<option value="STATIC">STATIC</option>
<option value="DYNAMIC">DYNAMIC</option>
</select>
</td>
<th style="width:80px;">사용</th>
<td>
<select name="useYn">
<option value="">전체</option>
<option value="Y">Y</option>
<option value="N">N</option>
</select>
</td>
</tr>
</tbody>
</table>
</form>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,365 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
var isDetail = false;
function isValid() {
if ($('input[name=cryptoName]').val().trim() === '') {
alert('모듈명을 입력하세요.');
$('input[name=cryptoName]').focus();
return false;
}
if ($('select[name=algType]').val() === '') {
alert('알고리즘을 선택하세요.');
return false;
}
if ($('select[name=cipherMode]').val() === '') {
alert('운영모드를 선택하세요.');
return false;
}
var keySourceType = $('select[name=keySourceType]').val();
if (keySourceType === '') {
alert('키 소스 유형을 선택하세요.');
return false;
}
if (keySourceType === 'STATIC') {
if ($('input[name=encKeyHex]').val().trim() === '') {
alert('암호화 키(Hex)를 입력하세요.');
$('input[name=encKeyHex]').focus();
return false;
}
}
if (keySourceType === 'DYNAMIC') {
if ($('input[name=keyDerivStrategy]').val().trim() === '') {
alert('키 도출 전략 FQCN을 입력하세요.');
$('input[name=keyDerivStrategy]').focus();
return false;
}
if ($('textarea[name=keyDerivParams]').val().trim() === '') {
alert('키 도출 파라미터(JSON)를 입력하세요.');
$('textarea[name=keyDerivParams]').focus();
return false;
}
}
return true;
}
function textToHex(text) {
if (!text) return '';
var encoder = new TextEncoder();
var bytes = encoder.encode(text);
return Array.from(bytes).map(function(b) {
return b.toString(16).padStart(2, '0');
}).join('').toUpperCase();
}
function updateHexFromText(textEl, hexName, lenSpanId) {
var hex = textToHex($(textEl).val());
$('input[name=' + hexName + ']').val(hex);
updateHexLength(hex, lenSpanId);
}
function updateHexLength(hex, lenSpanId) {
var charLen = hex ? hex.replace(/\s/g, '').length : 0;
var byteLen = Math.floor(charLen / 2);
var msg = charLen > 0 ? byteLen + ' bytes (' + charLen + '자)' : '';
$('#' + lenSpanId).text(msg);
}
function toggleKeySourceFields() {
var keySourceType = $('select[name=keySourceType]').val();
if (keySourceType === 'STATIC') {
$('.static-section').show();
$('.dynamic-section').hide();
} else if (keySourceType === 'DYNAMIC') {
$('.static-section').hide();
$('.dynamic-section').show();
} else {
$('.static-section').show();
$('.dynamic-section').show();
}
}
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: { cmd: 'DETAIL', cryptoId: key },
success: function(data) {
$('input[name=cryptoId]').val(data['CRYPTO_ID']);
$('input[name=cryptoName]').val(data['CRYPTO_NAME']).attr('readonly', true);
$('input[name=cryptoDesc]').val(data['CRYPTO_DESC']);
$('select[name=algType]').val(data['ALG_TYPE']);
$('select[name=cipherMode]').val(data['CIPHER_MODE']);
$('select[name=padding]').val(data['PADDING'] || '');
$('input[name=ivHex]').val(data['IV_HEX']);
$('select[name=keySourceType]').val(data['KEY_SOURCE_TYPE']);
$('input[name=encKeyHex]').val(data['ENC_KEY_HEX']);
$('input[name=decKeyHex]').val(data['DEC_KEY_HEX']);
$('input[name=keyDerivStrategy]').val(data['KEY_DERIV_STRATEGY']);
$('textarea[name=keyDerivParams]').val(data['KEY_DERIV_PARAMS']);
$('select[name=cacheYn]').val(data['CACHE_YN']);
$('input[name=cacheTtlSec]').val(data['CACHE_TTL_SEC']);
$('select[name=useYn]').val(data['USE_YN']);
updateHexLength(data['ENC_KEY_HEX'], 'encKeyLen');
updateHexLength(data['DEC_KEY_HEX'], 'decKeyLen');
updateHexLength(data['IV_HEX'], 'ivHexLen');
if (data['MODIFIED_BY'] || data['MODIFIED_AT']) {
$('#span_modified_by').text(data['MODIFIED_BY'] || '');
$('#span_modified_at').text((data['MODIFIED_AT'] || '').replace('T', ' '));
$('#row_modified_info').show();
}
toggleKeySourceFields();
},
error: function(e) {
alert(e.responseText);
}
});
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = '${param.cryptoId}';
if (key !== '' && key !== 'null') {
isDetail = true;
}
detail(url, key);
$('select[name=keySourceType]').change(function() {
toggleKeySourceFields();
});
$('input[name=encKeyHex]').on('input', function() {
updateHexLength($(this).val(), 'encKeyLen');
});
$('input[name=decKeyHex]').on('input', function() {
updateHexLength($(this).val(), 'decKeyLen');
});
$('input[name=ivHex]').on('input', function() {
updateHexLength($(this).val(), 'ivHexLen');
});
toggleKeySourceFields();
$('#btn_modify').click(function() {
if (!isValid()) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: 'cmd', value: isDetail ? 'UPDATE' : 'INSERT' });
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: postData,
success: function(data) {
var msg = '<%= localeMessage.getString("common.saveMsg") %>';
if (data && data.broadcastResult) {
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
}
alert(msg);
goNav(returnUrl);
},
error: function(e) {
alert(e.responseText);
}
});
});
$('#btn_delete').click(function() {
if (confirm('<%= localeMessage.getString("common.confirmMsg") %>') !== true) return;
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: { cmd: 'DELETE', cryptoId: $('input[name=cryptoId]').val() },
success: function(data) {
var msg = '<%= localeMessage.getString("common.deleteMsg") %>';
if (data && data.broadcastResult) {
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
}
alert(msg);
goNav(returnUrl);
},
error: function(e) {
alert(e.responseText);
}
});
});
$('#btn_previous').click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> 삭제</button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> 저장</button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> 이전</button>
</div>
<div class="title">암호화 모듈 설정 상세</div>
<form id="ajaxForm">
<input type="hidden" name="cryptoId" />
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;">모듈명 *</th>
<td><input type="text" name="cryptoName" style="width:300px;" /></td>
</tr>
<tr>
<th>설명</th>
<td><input type="text" name="cryptoDesc" style="width:400px;" /></td>
</tr>
<tr>
<th>알고리즘 *</th>
<td>
<select name="algType">
<option value="">선택</option>
<option value="AES">AES</option>
<option value="ARIA">ARIA</option>
</select>
</td>
</tr>
<tr>
<th>운영모드 *</th>
<td>
<select name="cipherMode">
<option value="">선택</option>
<option value="CBC">CBC</option>
<option value="GCM">GCM</option>
<option value="ECB">ECB</option>
<option value="FF1">FF1</option>
<option value="FF3-1">FF3-1</option>
</select>
</td>
</tr>
<tr>
<th>패딩</th>
<td>
<select name="padding">
<option value="">없음</option>
<option value="PKCS5Padding">PKCS5Padding</option>
<option value="NoPadding">NoPadding</option>
</select>
</td>
</tr>
<tr>
<th>IV (Hex)</th>
<td>
<input type="text" name="ivHex" style="width:380px;" placeholder="HEX 직접 입력" />
<span id="ivHexLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
<br/>
<input type="text" id="ivHexText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
oninput="updateHexFromText(this, 'ivHex', 'ivHexLen')" />
<span class="help-inline">CBC: 16바이트(32자) 필수 / GCM: 설정 시 기본 AAD로 사용</span>
</td>
</tr>
<tr>
<th>키 소스 유형 *</th>
<td>
<select name="keySourceType">
<option value="">선택</option>
<option value="STATIC">STATIC</option>
<option value="DYNAMIC">DYNAMIC</option>
</select>
</td>
</tr>
<!-- STATIC 전용 -->
<tr class="static-section">
<th>암호화 키 (Hex) *</th>
<td>
<input type="text" name="encKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
<span id="encKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
<br/>
<input type="text" id="encKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
oninput="updateHexFromText(this, 'encKeyHex', 'encKeyLen')" />
<span class="help-inline">AES-128: 16bytes(32자) / AES-192: 24bytes(48자) / AES-256: 32bytes(64자)</span>
</td>
</tr>
<tr class="static-section">
<th>복호화 키 (Hex)</th>
<td>
<input type="text" name="decKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
<span id="decKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
<br/>
<input type="text" id="decKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
oninput="updateHexFromText(this, 'decKeyHex', 'decKeyLen')" />
<span class="help-inline">미입력 시 암호화 키와 동일</span>
</td>
</tr>
<!-- DYNAMIC 전용 -->
<tr class="dynamic-section">
<th>키 도출 전략 (FQCN) *</th>
<td><input type="text" name="keyDerivStrategy" style="width:500px;" /></td>
</tr>
<tr class="dynamic-section">
<th>키 도출 파라미터 (JSON) *</th>
<td><textarea name="keyDerivParams" style="width:500px; height:80px;"></textarea></td>
</tr>
<tr>
<th>동적 키 캐시 *</th>
<td>
<select name="cacheYn">
<option value="N">N</option>
<option value="Y">Y</option>
</select>
</td>
</tr>
<tr>
<th>캐시 TTL (초)</th>
<td>
<input type="number" name="cacheTtlSec" style="width:100px;" />
<span class="help-inline">기본값 300</span>
</td>
</tr>
<tr>
<th>사용 여부 *</th>
<td>
<select name="useYn">
<option value="Y">Y</option>
<option value="N">N</option>
</select>
</td>
</tr>
<tr id="row_modified_info" style="display:none;">
<th>수정자 / 수정일시</th>
<td>
<span id="span_modified_by" style="font-weight:bold;"></span>
<span style="margin:0 8px; color:#ccc;">|</span>
<span id="span_modified_at" style="color:#555;"></span>
</td>
</tr>
</table>
</form>
</div>
</div>
</body>
</html>
@@ -39,12 +39,22 @@
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />'; var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />'; var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />';
function dateFormat(cellvalue) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
$(document).ready(function () { $(document).ready(function () {
var key = "${param.id}"; var key = "${param.id}";
if (key) { if (key) {
detail(key); detail(key);
} }
$("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
$("#btn_app_approve").click(function () { $("#btn_app_approve").click(function () {
if (confirm("승인하시겠습니까?")) { if (confirm("승인하시겠습니까?")) {
$.ajax({ $.ajax({
@@ -113,6 +123,28 @@
} }
}); });
$("#btn_modify").click(function () {
if (confirm("수정하시겠습니까?")) {
$.ajax({
type: "POST",
url: url,
data: {cmd: "UPDATE", id: key, expectEndDate: $("#expectEndDate").val().replaceAll('-','')},
success: function (args) {
alert("수정되었습니다.");
if (window.dialogArguments && window.dialogArguments.self) {
window.dialogArguments.self.location.reload();
} else if (window.opener) {
window.opener.location.reload();
}
window.close();
},
error: function (e) {
alert(e.responseText);
}
});
}
});
$("#btn_close").click(function () { $("#btn_close").click(function () {
window.close(); window.close();
}); });
@@ -126,12 +158,25 @@
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (json) { success: function (json) {
var data = json; var data = json;
if (data.approvalStatus.code == 'END') {
$('#btn_modify').hide();
$('#expectEndDate').hide();
$('#spExpectEndDate').show();
$('#spExpectEndDate').text(dateFormat(data.expectEndDate));
} else {
$('#btn_modify').show();
$('#expectEndDate').show();
$('#spExpectEndDate').hide();
}
$("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용"); $("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용");
$("#approvalStatus").text(data.approvalStatus.description); $("#approvalStatus").text(data.approvalStatus.description);
$("#requesterName").text(data.requester.userName); $("#requesterName").text(data.requester.userName);
$("#approvalSubject").text(data.approvalSubject); $("#approvalSubject").text(data.approvalSubject);
$("#approvalDetail").html(data.approvalDetail); $("#approvalDetail").html(data.approvalDetail);
$("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true}); $("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true});
$("#expectEndDate").val(data.expectEndDate);
if (data.approvers.some(approver => if (data.approvers.some(approver =>
approver.approverStatus === 'CURRENT' && approver.approverStatus === 'CURRENT' &&
@@ -144,7 +189,7 @@
document.getElementById("btn_app_reject").style.display = "none"; document.getElementById("btn_app_reject").style.display = "none";
} }
if (data.approvalStatus.description === '배포실패') { if (data.approvalStatus.code === 'FAILED') {
document.getElementById("btn_app_redeploy").style.display = ""; document.getElementById("btn_app_redeploy").style.display = "";
} }
@@ -242,9 +287,14 @@
</tr> </tr>
<tr> <tr>
<th>상세 내용</th> <th>상세 내용</th>
<td colspan="3"> <td>
<div id="approvalDetail" style="white-space: pre-wrap;"></div> <div id="approvalDetail" style="white-space: pre-wrap;"></div>
</td> </td>
<th>예상완료일</th>
<td>
<input type="text" id="expectEndDate" style="width: 120px; text-align:center"/>
<span id="spExpectEndDate"></span>
</td>
</tr> </tr>
</table> </table>
@@ -282,6 +332,9 @@
</table> </table>
</div> </div>
<div class="popup_button"> <div class="popup_button">
<button type="button" class="cssbtn" id="btn_modify" level="W"><i
class="material-icons">save</i>수정
</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_app_approve" level="W"><i <button type="button" class="cssbtn" style="display: none;" id="btn_app_approve" level="W"><i
class="material-icons">check</i>승인 class="material-icons">check</i>승인
</button> </button>
@@ -22,6 +22,14 @@
'APP': 'API 사용' 'APP': 'API 사용'
}; };
function dateFormat(cellvalue, options, rowObject) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
$(document).ready(function() { $(document).ready(function() {
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype: "json", datatype: "json",
@@ -33,7 +41,7 @@
searchApprovalStatus: $('select[name=searchApprovalStatus]').val(), searchApprovalStatus: $('select[name=searchApprovalStatus]').val(),
searchApprovalSubject: $('input[name=searchApprovalSubject]').val() searchApprovalSubject: $('input[name=searchApprovalSubject]').val()
}, },
colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일'], colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일', '예상완료일'],
colModel: [ colModel: [
{ name: 'rowNum', align: 'center', width: '30', sortable: false}, { name: 'rowNum', align: 'center', width: '30', sortable: false},
{ name: 'id', align: 'center', key: true, hidden: true }, { name: 'id', align: 'center', key: true, hidden: true },
@@ -42,7 +50,8 @@
{ name: 'approvalType', align: 'center', width: 80, hidden: true}, { name: 'approvalType', align: 'center', width: 80, hidden: true},
{ name: 'approvalStatus.description', align: 'center', width: 80 }, { name: 'approvalStatus.description', align: 'center', width: 80 },
{ name: 'requester.userName', align: 'center', width: 100 }, { name: 'requester.userName', align: 'center', width: 100 },
{ name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } } { name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } },
{ name: 'expectEndDate', align: 'center', width: 100, formatter: dateFormat},
], ],
jsonReader: { jsonReader: {
repeatitems: false repeatitems: false
@@ -134,8 +143,8 @@
<div class="select-style"> <div class="select-style">
<select name="searchApprovalStatus"> <select name="searchApprovalStatus">
<option value="">전체</option> <option value="">전체</option>
<option value="CREATED">생성됨</option>
<option value="REQUESTED">요청됨</option> <option value="REQUESTED">요청됨</option>
<option value="PROCESSING">진행중</option>
<option value="DENIED">반려됨</option> <option value="DENIED">반려됨</option>
<option value="END">승인됨</option> <option value="END">승인됨</option>
</select> </select>
@@ -20,15 +20,22 @@
var select_orgId = ""; var select_orgId = "";
function formatInquiryStatus(cellvalue, options, rowObject) { function formatInquiryStatus(cellvalue, options, rowObject) {
return cellvalue === 'PENDING' if (cellvalue == 'PENDING') {
? '<span style="color: red;">문의중</span>' return '<span style="color: red;">문의중</span>';
: '<span>답변완료</span>'; } else if (cellvalue == 'RESPONDED') {
return '<span >답변완료</span>'
} else if (cellvalue == 'CLOSED') {
return '<span >답변종료</span>'
} else {
return cellvalue;
}
} }
function formatFile(cellvalue, options, rowObject) { function formatFile(cellvalue, options, rowObject) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png'; var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : ''; var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
return cellvalue + icon; var commentCount = rowObject.commentCount > 0 ? ' <span style="color: #337ab7;">[' + rowObject.commentCount + ']</span>' : '';
return cellvalue + icon + commentCount;
} }
function deleteSelectedRows() { function deleteSelectedRows() {
@@ -37,6 +37,13 @@
border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */ border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */
} }
.comment-section {
margin-bottom: 30px;
border: 1px solid #e0e0e0; /* 동일한 박스 스타일 */
padding: 20px;
border-radius: 5px;
}
.info-row { .info-row {
margin-bottom: 10px; margin-bottom: 10px;
display: flex; display: flex;
@@ -58,6 +65,15 @@
min-height: 200px; min-height: 200px;
} }
#commentDetail {
min-height: 100px;
border: 1px solid #00000032;
border-radius: 4px;
padding: 10px;
flex: 1;
font-family: '맑은고딕'
}
.required-mark { .required-mark {
color: red; color: red;
margin-left: 2px; margin-left: 2px;
@@ -68,6 +84,7 @@
var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />'; var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />';
var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />'; var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />';
var file_download = '<c:url value="/file/download.do" />'; var file_download = '<c:url value="/file/download.do" />';
var inquiryStatus = 'PENDING';
function decodeHTMLEntities(text) { function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea'); var textArea = document.createElement('textarea');
@@ -82,10 +99,21 @@
dataType: "json", dataType: "json",
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (data) { success: function (data) {
$("#id").val(key); $("#id").val(key);
$("#inquirySubject").text(data.inquirySubject); $("#inquirySubject").text(data.inquirySubject);
$("#inquiryDetail").html(data.inquiryDetail); $("#inquiryDetail").html(data.inquiryDetail);
$("#inquiryStatus").text(data.inquiryStatus === 'PENDING' ? '문의중' : '답변완료');
inquiryStatus = data.inquiryStatus;
if (data.inquiryStatus == 'PENDING') {
$("#inquiryStatus").text('문의중');
} else if (data.inquiryStatus == 'RESPONDED') {
$("#inquiryStatus").text('답변완료');
} else if (data.inquiryStatus == 'CLOSED') {
$("#inquiryStatus").text('답변종료');
}
$("#inquirer").text(data.inquirer.userName); $("#inquirer").text(data.inquirer.userName);
$("#createdDate").text(timeStampFormat(data.createdDate)); $("#createdDate").text(timeStampFormat(data.createdDate));
$("#responder").text(data.responderName || ''); $("#responder").text(data.responderName || '');
@@ -94,6 +122,8 @@
var decodedContent = decodeHTMLEntities(data.responseDetail); var decodedContent = decodeHTMLEntities(data.responseDetail);
$("#responseDetail").summernote('code', decodedContent || ''); $("#responseDetail").summernote('code', decodedContent || '');
// 댓글 표시
listComment(key);
// 첨부 파일 표시 // 첨부 파일 표시
displayAttachFiles(data.fileInfo); displayAttachFiles(data.fileInfo);
@@ -105,6 +135,59 @@
}); });
} }
function listComment(inquiryId) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_COMMENT', id: inquiryId},
success: function (data) {
var $commentList = $('#commentList');
$commentList.empty();
if (!data || data.length === 0) return;
$.each(data, function (i, comment) {
var date = comment.createdDate ? timeStampFormat(comment.createdDate) : '';
var row = $('<div>').addClass('info-row');
$('<div>').addClass('info-label').css('color', '#555').text(comment.writerName || '').appendTo(row);
var content = $('<div>').addClass('info-content');
$('<div>').css('white-space', 'pre-wrap').text(comment.commentDetail).appendTo(content);
var meta = $('<div>').css({'font-size': '0.9em', 'color': '#999', 'margin-top': '2px'});
$('<span>').text(date).appendTo(meta);
$('<button>').attr('type', 'button').addClass('cssbtn smallBtn2').css({'margin-left': '8px', 'font-size': '0.85em', 'padding': '1px 6px'})
.text('delete').on('click', (function(id, inquiryId) {
return function() { deleteComment(id, inquiryId); };
})(comment.id, comment.inquiryId))
.appendTo(meta);
meta.appendTo(content);
content.appendTo(row);
row.appendTo($commentList);
});
},
error: function (e) {
alert(e.responseText);
}
});
}
function deleteComment(commentId, inquiryId) {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 삭제할 수 없습니다');
return;
}
if (!confirm("댓글을 삭제하시겠습니까?")) return;
$.ajax({
type: "POST",
url: url,
data: {cmd: 'DELETE_COMMENT', id: commentId},
success: function () {
listComment(inquiryId);
},
error: function (e) {
alert(e.responseText);
}
});
}
$(document).ready(function () { $(document).ready(function () {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key = "${param.id}"; var key = "${param.id}";
@@ -115,11 +198,16 @@
height: 200 height: 200
}); });
if (key) { if (key) {
detail(key); detail(key);
} }
$("#btn_modify").click(function () { $("#btn_modify").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 수정할 수 없습니다');
return;
}
if (!checkRequired("ajaxForm")) return; if (!checkRequired("ajaxForm")) return;
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return; if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
@@ -140,6 +228,32 @@
}); });
}); });
$("#btn_comment").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 댓글을 등록할 수 없습니다');
return;
}
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
$.ajax({
type: "POST",
url: url,
data: {
cmd: "INSERT_COMMENT",
inquiryId: $('#id').val(),
commentDetail: $('#commentDetail').val(),
},
success: function (json) {
alert("<%= localeMessage.getString("common.saveMsg") %>");
$('#commentDetail').val('');
listComment(key);
},
error: function (e) {
alert(e.responseText);
}
});
});
$("#btn_previous").click(function () { $("#btn_previous").click(function () {
goNav(returnUrl); goNav(returnUrl);
}); });
@@ -206,6 +320,10 @@
<!-- 답변 작성 부분 --> <!-- 답변 작성 부분 -->
<div class="response-section"> <div class="response-section">
<div class="info-row">
<div class="info-label">상태</div>
<div id="inquiryStatus" class="info-content"></div>
</div>
<div class="info-row"> <div class="info-row">
<div class="info-label">답변자</div> <div class="info-label">답변자</div>
<div id="responder" class="info-content"></div> <div id="responder" class="info-content"></div>
@@ -221,6 +339,19 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 댓글 작성 부분 -->
<div class="comment-section">
<div id="commentList"></div>
<div class="info-row">
<div class="info-label">${sessionScope.login.userName}</div>
<div style="flex:1; display: flex; gap: 8px; align-items: stretch;">
<textarea id="commentDetail" name="commentDetail" data-required data-warning="댓글 내용을 입력하여 주십시오."></textarea>
<button type="button" class="cssbtn" id="btn_comment" level="W" style="width:100px; height: 100px; text-align:center; ">댓글등록</button>
</div>
</div>
</div>
<input type="hidden" name="id" id="id"> <input type="hidden" name="id" id="id">
</form> </form>
</div> </div>
@@ -17,6 +17,17 @@
<script language="javascript" > <script language="javascript" >
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />'; var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />'; var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
var combo;
function formatNoticeType(cellvalue, options, rowObject) {
var name = "";
for (var i = 0; i < combo.noticeTypeList.length; i++) {
if (combo.noticeTypeList[i].CODE == cellvalue) {
return combo.noticeTypeList[i].NAME;
}
}
return cellvalue;
}
function formatuseYn(cellvalue, options, rowObject) { function formatuseYn(cellvalue, options, rowObject) {
return cellvalue === 'N' return cellvalue === 'N'
@@ -24,27 +35,57 @@
: '<span>사용</span>'; : '<span>사용</span>';
} }
function formatFixYn(cellvalue, options, rowObject) {
return cellvalue === 'Y'
? '고정'
: '';
}
function formatFile(cellvalue, options, rowObject) { function formatFile(cellvalue, options, rowObject) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png'; var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : ''; var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
return cellvalue + icon; return cellvalue + icon;
} }
function init(){
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
console.log('init', json);
combo = json;
new makeOptions("CODE","NAME").setObj($("select[name=searchNoticeType]")).setNoValueInclude(true).setNoValue('','<%=localeMessage.getString("combo.all")%>').setData(json.noticeTypeList).rendering();
putSelectFromParam();
},
error:function(e){
alert(e.responseText);
}
});
}
$(document).ready(function() { $(document).ready(function() {
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData : { cmd : 'LIST', postData : { cmd : 'LIST',
searchNoticeType: $('select[name=searchNoticeType]').val(),
searchUseYn: $('select[name=searchUseYn]').val(), searchUseYn: $('select[name=searchUseYn]').val(),
searchNoticeSubject: $('input[name=searchNoticeSubject]').val(), searchNoticeSubject: $('input[name=searchNoticeSubject]').val(),
searchNoticeDetail: $('input[name=searchNoticeDetail]').val() searchNoticeDetail: $('input[name=searchNoticeDetail]').val()
}, },
colNames:['id', 'No.', '제목', '사용', '마지막 수정일', '등록일', '등록자', '조회수'], colNames:['id', 'No.', '게시유형', '제목', '고정여부', '사용', '마지막 수정일', '등록일', '등록자', '조회수'],
colModel:[ colModel:[
{ name : 'id' , align:'center', key:true, hidden:true}, { name : 'id' , align:'center', key:true, hidden:true},
{ name : 'rowNum' , align:'center', width:80 }, { name : 'rowNum' , align:'center', width:60 },
{ name : 'noticeSubject' , align:'left' , width:200 , formatter: formatFile }, { name : 'noticeType' , align:'center', width:60, formatter: formatNoticeType },
{ name : 'noticeSubject' , align:'left' , width:300, formatter: formatFile },
{ name : 'fixYn' , align:'center', width:60, formatter: formatFixYn },
{ name : 'useYn' , align:'center', width:60, formatter: formatuseYn }, { name : 'useYn' , align:'center', width:60, formatter: formatuseYn },
{ name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat}, { name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat},
{ name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat }, { name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat },
@@ -98,6 +139,8 @@
} }
}); });
init();
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){ $("#btn_search").click(function(){
@@ -140,9 +183,25 @@
<div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div> <div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div>
<form id="ajaxForm" onsubmit="return false;"> <form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing=0;> <table class="search_condition" cellspacing=0;>
<colgroup>
<col style="width:180px;">
<col style="width:240px;">
<col style="width:180px;">
<col style="width:240px;">
<col style="width:180px;">
<col style="width:240px;">
</colgroup>
<tbody> <tbody>
<tr> <tr>
<th style="width:180px;">사용여부</th> <th>게시유형</th>
<td>
<div class="select-style">
<select name="searchNoticeType"></select>
</div>
</td>
<th>제목/내용</th>
<td><input type="text" name="searchSubjectDetail"></td>
<th>사용여부</th>
<td> <td>
<div class="select-style"> <div class="select-style">
<select name="searchUseYn"> <select name="searchUseYn">
@@ -152,8 +211,6 @@
</select> </select>
</div> </div>
</td> </td>
<th style="width:180px;">제목/내용</th>
<td><input type="text" name="searchSubjectDetail"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -24,7 +24,36 @@
var fileInfo = null; var fileInfo = null;
function init() { function init() {
var key = "${param.id}";
isDetail = key != "" && key != "null";
if (isDetail) {
$("#title").append(" 수정");
buttonControl(true);
} else {
$("#title").append(" 등록");
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
buttonControl(false);
}
// 초기화 로직 // 초기화 로직
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
combo = json;
new makeOptions("CODE","NAME").setObj($("select[name=noticeType]")).setNoValueInclude(false).setData(json.noticeTypeList).rendering();
if (key) {
detail(key);
}
},
error:function(e){
alert(e.responseText);
}
});
} }
function decodeHTMLEntities(text) { function decodeHTMLEntities(text) {
@@ -98,8 +127,10 @@
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (data) { success: function (data) {
$("#id").val(key); $("#id").val(key);
$('#noticeType').val(data.noticeType);
$("#noticeSubject").val(data.noticeSubject); $("#noticeSubject").val(data.noticeSubject);
$("#useYn").prop("checked", data.useYn === "Y"); $("#useYn").prop("checked", data.useYn === "Y");
$("#fixYn").prop("checked", data.fixYn === "Y");
var decodedContent = decodeHTMLEntities(data.noticeDetail); var decodedContent = decodeHTMLEntities(data.noticeDetail);
$('#contents').summernote('code', decodedContent); $('#contents').summernote('code', decodedContent);
@@ -123,29 +154,16 @@
$(document).ready(function () { $(document).ready(function () {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key = "${param.id}";
isDetail = key != "" && key != "null";
if (isDetail) {
$("#title").append(" 수정");
buttonControl(true);
} else {
$("#title").append(" 등록");
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
buttonControl(false);
}
init(); init();
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원) // Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
initSummernote('#contents', { initSummernote('#contents', {
placeholder: '여기에 내용을 입력하세요.', placeholder: '여기에 내용을 입력하세요.',
height: 300 height: 300
}); });
if (key) {
detail(key);
}
$('#fileInput').change(function(e) { $('#fileInput').change(function(e) {
var file = e.target.files[0]; var file = e.target.files[0];
@@ -175,6 +193,7 @@
var formData = new FormData($("#ajaxForm")[0]); var formData = new FormData($("#ajaxForm")[0]);
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N"); formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N");
formData.set("noticeDetail", $('#contents').summernote('code')); formData.set("noticeDetail", $('#contents').summernote('code'));
formData.append("cmd", isDetail ? "UPDATE" : "INSERT"); formData.append("cmd", isDetail ? "UPDATE" : "INSERT");
@@ -244,31 +263,50 @@
<form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8"> <form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8">
<input type="hidden" name="id" id="id"> <input type="hidden" name="id" id="id">
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<colgroup>
<col style="width: 10%"/>
<col style="width: 40%"/>
<col style="width: 10%"/>
<col style="width: 40%"/>
</colgroup>
<tr> <tr>
<th style="width:20%;">사용여부</th> <th>게시유형</th>
<td> <td colspan="3">
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용</td> <div class="select-style" >
</tr> <select name="noticeType" id="noticeType"></select>
<tr> </div>
<th style="width:20%;">제목 <font color="red">*</font></th>
<td><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
</tr>
<tr>
<th style="width:20%;">본문 <font color="red">*</font></th>
<td>
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;">첨부파일</th> <th>제목 <font color="red">*</font></th>
<td colspan="3"><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
</tr>
<tr>
<th>사용여부</th>
<td> <td>
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용
</td>
<th>고정여부</th>
<td>
<input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정
</td>
</tr>
<tr>
<th>본문 <font color="red">*</font></th>
<td colspan="3">
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td>
</tr>
<!-- <tr>
<th>첨부파일</th>
<td colspan="3">
<div style="margin: 5px 0px; display: inline-block;"> <div style="margin: 5px 0px; display: inline-block;">
<input type="file" id="fileInput" name="files" style="display:none;" /> <input type="file" id="fileInput" name="files" style="display:none;" />
<button type="button" id="addFile" class="cssbtn smallBtn">파일 선택</button> <button type="button" id="addFile" class="cssbtn smallBtn">파일 선택</button>
</div> </div>
<div id="attachFiles" style="margin: 5px 0px; display: inline-block;"></div> <div id="attachFiles" style="margin: 5px 0px; display: inline-block;"></div>
</td> </td>
</tr> </tr> -->
</table> </table>
</form> </form>
</div> </div>
@@ -82,6 +82,54 @@
</style> </style>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript"> <script language="javascript">
function executeAggregationHourly() {
var targetDate = $("#targetHour").val().replace(/-/g, "");
var resultDiv = $("#hourlyResult");
// 날짜 입력 검증
if (!targetDate || targetDate.trim() === '') {
alert('대상 날짜를 입력하세요. (예: 20250120)');
$("#targetHour").focus();
return;
}
resultDiv.hide();
$("#btn_execute_hourly").prop("disabled", true).text("실행중...");
$.ajax({
url: '<c:url value="/onl/kjb/statistics/apiStatsAggregationMan.json"/>',
type: 'POST',
data: {
cmd: 'AGGREGATION_HOUR',
targetDate: targetDate,
serviceType: '${param.serviceType}'
},
success: function(response) {
resultDiv.removeClass("error").addClass("success");
var message = response.message + "\n";
message += "대상 날짜: " + response.targetDate + "\n";
message += "처리 건수: " + response.processedCount;
resultDiv.find("pre").text(message);
resultDiv.show();
},
error: function(xhr) {
resultDiv.removeClass("success").addClass("error");
var message = "집계 실행 실패\n";
try {
var error = JSON.parse(xhr.responseText);
message += error.message;
} catch(e) {
message += xhr.responseText || "알 수 없는 오류가 발생했습니다.";
}
resultDiv.find("pre").text(message);
resultDiv.show();
},
complete: function() {
$("#btn_execute_hourly").prop("disabled", false).text("집계 실행");
}
});
}
function executeHourlyToDaily() { function executeHourlyToDaily() {
var targetDate = $("#targetDate").val().replace(/-/g, ""); var targetDate = $("#targetDate").val().replace(/-/g, "");
var resultDiv = $("#dailyResult"); var resultDiv = $("#dailyResult");
@@ -241,6 +289,7 @@
var year = yesterday.getFullYear(); var year = yesterday.getFullYear();
var month = String(yesterday.getMonth() + 1).padStart(2, '0'); var month = String(yesterday.getMonth() + 1).padStart(2, '0');
var day = String(yesterday.getDate()).padStart(2, '0'); var day = String(yesterday.getDate()).padStart(2, '0');
$("#targetHour").val(year + month + day);
$("#targetDate").val(year + month + day); $("#targetDate").val(year + month + day);
// 전월 // 전월
@@ -255,6 +304,12 @@
$("#targetYear").val(lastYear); $("#targetYear").val(lastYear);
// 버튼 이벤트 // 버튼 이벤트
$("#btn_execute_hourly").click(function() {
if (confirm("시간별 통계 집계를 실행하시겠습니까?")) {
executeAggregationHourly();
}
});
$("#btn_execute_daily").click(function() { $("#btn_execute_daily").click(function() {
if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) { if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) {
executeHourlyToDaily(); executeHourlyToDaily();
@@ -297,9 +352,26 @@
</ul> </ul>
</div> </div>
<!-- 거래로그 → 시간별 집계 -->
<div class="aggregation-section">
<h3>1. 거래로그 → 시간별 통계 집계</h3>
<div class="aggregation-form">
<label>대상 날짜:</label>
<input type="text" id="targetHour" placeholder="yyyyMMdd" maxlength="8">
<span style="color: #666; font-size: 12px;">(예: 20250120)</span>
<button type="button" class="cssbtn" id="btn_execute_hourly" level="W">
<i class="material-icons">play_arrow</i> 집계 실행
</button>
</div>
<div id="hourlyResult" class="result-area">
<strong>실행 결과:</strong>
<pre></pre>
</div>
</div>
<!-- 시간별 → 일별 집계 --> <!-- 시간별 → 일별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>1. 시간별 → 일별 통계 집계</h3> <h3>2. 시간별 → 일별 통계 집계</h3>
<div class="aggregation-form"> <div class="aggregation-form">
<label>대상 날짜:</label> <label>대상 날짜:</label>
<input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8"> <input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8">
@@ -316,7 +388,7 @@
<!-- 일별 → 월별 집계 --> <!-- 일별 → 월별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>2. 일별 → 월별 통계 집계</h3> <h3>3. 일별 → 월별 통계 집계</h3>
<div class="aggregation-form"> <div class="aggregation-form">
<label>대상 월:</label> <label>대상 월:</label>
<input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6"> <input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6">
@@ -333,7 +405,7 @@
<!-- 월별 → 연별 집계 --> <!-- 월별 → 연별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>3. 월별 → 연별 통계 집계</h3> <h3>4. 월별 → 연별 통계 집계</h3>
<div class="aggregation-form"> <div class="aggregation-form">
<label>대상 연도:</label> <label>대상 연도:</label>
<input type="text" id="targetYear" placeholder="yyyy" maxlength="4"> <input type="text" id="targetYear" placeholder="yyyy" maxlength="4">
@@ -40,7 +40,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,9 +54,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -73,7 +71,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -98,8 +96,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -118,64 +115,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -191,32 +131,13 @@
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
} }
function updateCharts(data) { function updateCharts(data) {
@@ -268,8 +189,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -279,22 +199,6 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -302,24 +206,13 @@
series: [ series: [
{ data: successData }, { data: successData },
{ data: timeoutData }, { data: timeoutData },
{ data: systemErrData }, { data: systemErrData }
{ data: bizErrData }
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(6, 8) + '일'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -535,8 +428,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -545,10 +437,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -580,9 +468,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -596,15 +483,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -685,9 +566,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -712,7 +591,7 @@
<tbody> <tbody>
<tr> <tr>
<th style="width:100px;">조회기간</th> <th style="width:100px;">조회기간</th>
<td colspan="3"> <td colspan="5">
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;"> <input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;">
~ ~
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;"> <input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;">
@@ -724,22 +603,20 @@
<td> <td>
<input type="text" name="searchApiName" value="${param.searchApiName}"> <input type="text" name="searchApiName" value="${param.searchApiName}">
</td> </td>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">인스턴스</th> <th style="width:100px;">인스턴스</th>
<td> <td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}"> <input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th> <th style="width:100px;">클라이언트ID</th>
<td> <td>
<input type="text" name="searchClientId" value="${param.searchClientId}"> <input type="text" name="searchClientId" value="${param.searchClientId}">
</td> </td>
</tr>
<tr>
<th style="width:100px;">Inbound Adapter</th> <th style="width:100px;">Inbound Adapter</th>
<td> <td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}"> <input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
@@ -754,12 +631,7 @@
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
@@ -41,7 +41,7 @@
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>';
var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>'; var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -55,9 +55,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -74,7 +72,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -99,8 +97,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -119,64 +116,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -185,47 +125,24 @@
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
}, },
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 }, legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
// 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동) // 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동)
callChart.on('click', function(params) { callChart.on('click', function(params) {
drillDownToMinute(callChart, params.dataIndex); drillDownToMinute(callChart, params.dataIndex);
}); });
respChart.on('click', function(params) {
drillDownToMinute(respChart, params.dataIndex);
});
} }
function drillDownToMinute(chart, dataIndex) { function drillDownToMinute(chart, dataIndex) {
@@ -308,8 +225,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -319,22 +235,6 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -342,24 +242,13 @@
series: [ series: [
{ data: successData }, { data: successData },
{ data: timeoutData }, { data: timeoutData },
{ data: systemErrData }, { data: systemErrData }
{ data: bizErrData }
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(8, 10) + '시'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -541,8 +430,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -551,10 +439,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -586,9 +470,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -602,15 +485,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -669,9 +546,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -736,12 +611,7 @@
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
@@ -10,7 +10,7 @@
%> %>
<html> <html>
<head> <head>
<title>분별 통계 조회</title> <title>대시보드</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <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/css.jsp"/>
<style> <style>
@@ -33,7 +33,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -47,9 +47,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -66,7 +64,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -91,8 +89,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -111,64 +108,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -177,7 +117,7 @@
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
}, },
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 }, legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
@@ -188,34 +128,12 @@
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [], showSymbol: false }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [], showSymbol: false },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [], showSymbol: false }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [], showSymbol: false }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [], showSymbol: false }
] ]
}; };
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 }
],
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [], showSymbol: false },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [], showSymbol: false }
]
};
callChart.setOption(callOption); callChart.setOption(callOption);
respChart.setOption(respOption);
} }
function updateCharts(data) { function updateCharts(data) {
@@ -269,8 +187,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -280,22 +197,6 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -303,20 +204,10 @@
series: [ series: [
{ data: successData }, { data: successData },
{ data: timeoutData }, { data: timeoutData },
{ data: systemErrData }, { data: systemErrData }
{ data: bizErrData }
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
} }
// 조회 기간 검증 및 조정 (최대 1시간) // 조회 기간 검증 및 조정 (최대 1시간)
@@ -552,8 +443,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -562,10 +452,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -597,9 +483,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -613,15 +498,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -711,9 +590,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -733,7 +610,7 @@
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %> <i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button> </button>
</div> </div>
<div class="title" id="title">API 분단위 통계</div> <div class="title" id="title">대시보드</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
<tbody> <tbody>
<tr> <tr>
@@ -747,72 +624,28 @@
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span> <span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span>
</td> </td>
</tr> </tr>
<tr>
<th style="width:100px;">API명</th>
<td>
<input type="text" name="searchApiName" value="${param.searchApiName}">
</td>
<th style="width:100px;">인스턴스</th>
<td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td>
</tr>
<tr>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th>
<td>
<input type="text" name="searchClientId" value="${param.searchClientId}">
</td>
</tr>
<tr>
<th style="width:100px;">Inbound Adapter</th>
<td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
</td>
<th style="width:100px;">Outbound Adapter</th>
<td>
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
</td>
</tr>
</tbody> </tbody>
</table> </table>
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div class="title" style="margin: 0;">API요약 통계</div> <div class="title" style="margin: 0;">API별 사용현황</div>
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R"> <button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
<i class="material-icons">file_download</i> Excel 다운로드 (요약) <i class="material-icons">file_download</i> Excel 다운로드
</button> </button>
</div> </div>
<table id="gridSummary"></table> <table id="gridSummary"></table>
<div id="pagerSummary"></div> <div id="pagerSummary"></div>
</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;">상세 통계</div>
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
<i class="material-icons">file_download</i> Excel 다운로드 (상세)
</button>
</div>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div> </div>
</div> </div>
</body> </body>
@@ -40,7 +40,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,9 +54,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -73,7 +71,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -98,8 +96,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -118,64 +115,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -184,39 +124,18 @@
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
}, },
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 }, legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
} }
function updateCharts(data) { function updateCharts(data) {
@@ -268,8 +187,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -279,22 +197,7 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -307,19 +210,10 @@
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(4, 6) + '월'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -539,8 +433,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -549,10 +442,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -584,9 +473,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -600,15 +488,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -675,9 +557,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -744,14 +624,10 @@
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
@@ -40,7 +40,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,9 +54,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -73,7 +71,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -98,8 +96,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -118,64 +115,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -184,39 +124,20 @@
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
}, },
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 }, legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
} }
function updateCharts(data) { function updateCharts(data) {
@@ -268,8 +189,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -279,22 +199,7 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -307,19 +212,10 @@
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(0, 4) + '년'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -513,8 +409,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -523,10 +418,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -558,9 +449,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -574,15 +464,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -644,9 +528,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -712,12 +594,7 @@
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
@@ -0,0 +1,313 @@
<%@ 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");
request.setCharacterEncoding("UTF-8");
%>
<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 src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
<script language="javascript">
var url = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.view"/>';
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
return Number(cellvalue).toLocaleString();
}
function decimalFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
return (cellvalue).toFixed(2);
}
function getPostData() {
var postData = {}
if (arguments.length == 2) {
postData[arguments[0]] = arguments[1];
}
var searchStartDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDate]").val().replace(/-/g, "");
if (searchStartDate && searchEndDate) {
var start = new Date(searchStartDate.substring(0, 4), parseInt(searchStartDate.substring(4, 6)) - 1, searchStartDate.substring(6, 8));
var end = new Date(searchEndDate.substring(0, 4), parseInt(searchEndDate.substring(4, 6)) - 1, searchEndDate.substring(6, 8));
var diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
if (diffDays > 31) {
alert('조회 기간은 최대 31일까지 가능합니다.');
return null;
}
}
postData.searchType = $('input[name="searchType"]:checked').val();
postData.searchOrgName = $('#searchOrgName').val();
postData.searchApiName = $('#searchApiName').val();
postData.searchStartDateTime = searchStartDate;
postData.searchEndDateTime = searchEndDate;
return postData;
}
function search() {
var postData = getPostData("cmd", "LIST");
if (postData) {
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
}
}
function exportToExcel() {
var postData = getPostData("cmd", "EXCEL_EXPORT");
if (!postData) {
return;
}
postData.serviceType = '${param.serviceType}';
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 추출
var filename = 'api_stats.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
console.log('reader.result', reader.result)
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() {
var gridPostData = getPostData("cmd", "LIST");
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
postData: gridPostData,
colNames: [
'구분',
'총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
{ name: 'orgName', 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: 'successRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'failRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: 'auto',
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
loadComplete: function(d) {
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false });
}
}
});
}
$(document).ready(function() {
// 날짜/시간 입력 마스크
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
var today = getToday();
var startDate = today;
var endDate = today;
if (!$("input[name=searchStartDate]").val()) {
$("input[name=searchStartDate]").val(startDate);
}
if (!$("input[name=searchEndDate]").val()) {
$("input[name=searchEndDate]").val(endDate);
}
list();
resizeJqGridWidth('grid', 'content_middle', '1200');
$("#btn_search").click(function() {
search();
});
$("#btn_excel").click(function() {
exportToExcel();
});
$("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>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_excel" level="R" style="display: inline-block;"><i class="material-icons">table_view</i> 엑셀</button>
<button type="button" class="cssbtn" id="btn_search" level="R" style="display: inline-block;">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button>
</div>
<div class="title" id="title">API 사용현황</div>
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
<th style="width:120px;">조회기간</th>
<td colspan="5">
<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;">
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
</td>
</tr>
<tr>
<th style="width:120px;">조회구분</th>
<td>
<input type="radio" name="searchType" id="searchTypeORG" value="ORG" checked="checked"><label for="searchTypeORG">제휴사</label></input>
<input type="radio" name="searchType" id="searchTypeAPI" value="API"><label for="searchTypeAPI">API</label></input>
<input type="radio" name="searchType" id="searchTypeDATE" value="DATE"><label for="searchTypeDATE">사용일</label></input>
</td>
<th style="width:120px;">제휴사명</th>
<td>
<input type="text" name="searchOrgName" id="searchOrgName" value="${param.searchOrgName}">
</td>
<th style="width:120px;">API명</th>
<td>
<input type="text" name="searchApiName" id="searchApiName" value="${param.searchApiName}">
</td>
</tr>
</tbody>
</table>
<div>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div>
</div>
</body>
</html>
@@ -411,7 +411,7 @@
function detail(url,key){ function detail(url,key){
jsonUrl = url; jsonUrl = url;
if (!isDetail)return; if (!isDetail)return;
$("input[name='btnRadioSyncAsync']").attr('disabled', true); // $("input[name='btnRadioSyncAsync']").attr('disabled', true);
$.ajax({ $.ajax({
type : "POST", type : "POST",
url:url, url:url,
@@ -962,11 +962,13 @@
headerNames = messageKeyList; headerNames = messageKeyList;
isHeaderRouting = true; isHeaderRouting = true;
initHeaders(headerNames); initHeaders(headerNames);
$('#slideInboundRoutingRule').carousel(0); //$('#slideInboundRoutingRule').carousel(0);
$('#collapseInboundRoutingInfo').collapse('show')
setHeaderRoutingLabel(); setHeaderRoutingLabel();
}else{ }else{
isHeaderRouting = false; isHeaderRouting = false;
$('#slideInboundRoutingRule').carousel(1); $('#collapseInboundRoutingInfo').collapse('hide')
//$('#slideInboundRoutingRule').carousel(1);
} }
} }
@@ -1572,7 +1574,7 @@
<fieldset class="groupbox-border"> <fieldset class="groupbox-border">
<legend class="groupbox-border">INTERFACE TYPE</legend> <legend class="groupbox-border">INTERFACE TYPE</legend>
<div class="row"> <div class="row">
<div class="form-group col-md-3"> <!-- <div class="form-group col-md-3">
<label for="eaiBzwkDstcd"><span class="material-icons-outlined">sync</span> Sync/Async 타입</label><br> <label for="eaiBzwkDstcd"><span class="material-icons-outlined">sync</span> Sync/Async 타입</label><br>
<div class="btn-group" id="btnGroupSyncAsync" role="group"> <div class="btn-group" id="btnGroupSyncAsync" role="group">
<input type="radio" <input type="radio"
@@ -1615,19 +1617,19 @@
<i class="bi bi-share-fill"></i> S > A <i class="bi bi-share-fill"></i> S > A
</label> </label>
</div> </div>
</div> </div> -->
<div class="form-group col-md-3"> <div class="form-group col-md-3">
<label for="eaiBzwkDstcd">요청/응답 구분</label><br> <label for="eaiBzwkDstcd">요청/응답 구분</label><br>
<div class="btn-group" role="group" aria-label="Basic radio toggle button group" <div class="btn-group" role="group" aria-label="Basic radio toggle button group"
x-effect="if (apiInterface.syncAsyncType !== 'async') apiInterface.requestType = 'S'"> x-effect="if (apiInterface.syncAsyncType !== 'async') apiInterface.requestType = 'S'">
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeRequest" <input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeRequest"
x-model="apiInterface.requestType" x-model="apiInterface.requestType"
value="S" :disabled="apiInterface.syncAsyncType !== 'async' || $store.formState.isReqResDisabled"> value="S" :disabled="$store.formState.isReqResDisabled">
<label class="btn btn-outline-primary" for="btnTypeRequest"><i class="bi bi-arrow-bar-right"></i> 요청</label> <label class="btn btn-outline-primary" for="btnTypeRequest"><i class="bi bi-arrow-bar-right"></i> 요청</label>
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeResponse" <input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeResponse"
x-model="apiInterface.requestType" x-model="apiInterface.requestType"
value="R" :disabled="apiInterface.syncAsyncType !== 'async' || $store.formState.isReqResDisabled"> value="R" :disabled="$store.formState.isReqResDisabled">
<label class="btn btn-outline-primary" for="btnTypeResponse"><i class="bi bi-arrow-bar-left"></i> 응답</label> <label class="btn btn-outline-primary" for="btnTypeResponse"><i class="bi bi-arrow-bar-left"></i> 응답</label>
</div> </div>
</div> </div>
@@ -1771,21 +1773,7 @@
</select> </select>
</div> </div>
<div class="form-group col-md-8"> <div class="form-group col-md-8">
<div id="slideInboundRoutingRule" class="carousel slide" data-ride="carousel" data-interval="false"> <div id="slideInboundRoutingRule">
<div class="carousel-inner" style="margin-bottom:-20px">
<div class="carousel-item">
<label>
<span class="material-icons justify-content-md-start">http</span>
수신 라우팅 정보
</label>
<div class="d-grid gap-2">
<button type="button" id="headerRoutingButton" class="btn btn-primary"
data-bs-toggle="modal" data-bs-target="#routingModal">
수정
</button>
</div>
</div>
<div class="carousel-item active">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<label for="inboundRestPath"> <label for="inboundRestPath">
@@ -1799,6 +1787,18 @@
</div> </div>
</div> </div>
</div> </div>
<div class="collapse mt-2" id="collapseInboundRoutingInfo">
<div class="form-group col-md-12">
<label>
<span class="material-icons justify-content-md-start">http</span>
수신 라우팅 정보
</label>
<div class="d-grid gap-2">
<button type="button" id="headerRoutingButton" class="btn btn-primary"
data-bs-toggle="modal" data-bs-target="#routingModal">
수정
</button>
</div>
</div> </div>
</div> </div>
<div id="inboundResponseMethodPathRow" <div id="inboundResponseMethodPathRow"
+3 -7
View File
@@ -15,7 +15,7 @@ def quartzVersion = "2.2.1"
def queryDslVersion = "5.0.0" def queryDslVersion = "5.0.0"
def hibernateVersion = "5.6.15.Final" def hibernateVersion = "5.6.15.Final"
def reposiliteUrl = "http://localhost:8080" def nexusUrl = "https://nexus.eactive.synology.me:8090"
//def useOnJboss = false //def useOnJboss = false
def generatedJavaDir = "$buildDir/generated/java" def generatedJavaDir = "$buildDir/generated/java"
@@ -23,12 +23,8 @@ def generatedJavaDir = "$buildDir/generated/java"
allprojects { allprojects {
repositories { repositories {
maven { maven {
url "${reposiliteUrl}/private" url "${nexusUrl}/repository/maven-public/"
allowInsecureProtocol = true allowInsecureProtocol = true
credentials {
username = reposiliteUser
password = reposilitePassword
}
} }
} }
} }
@@ -129,7 +125,7 @@ dependencies {
implementation "org.snmp4j:snmp4j:1.10.1" implementation "org.snmp4j:snmp4j:1.10.1"
implementation "com.googlecode.json-simple:json-simple:1.1.1" implementation "com.googlecode.json-simple:json-simple:1.1.1"
implementation "io.netty:netty-all:4.1.0.Final" implementation "io.netty:netty-all:4.1.94.Final"
compileOnly "org.apache.mina:mina-filter-ssl:1.1.6" compileOnly "org.apache.mina:mina-filter-ssl:1.1.6"
compileOnly "org.apache.mina:mina-core:1.1.6" compileOnly "org.apache.mina:mina-core:1.1.6"
+11
View File
@@ -0,0 +1,11 @@
import oracledb
conn = oracledb.connect(user='AGWAPP', password='elink1234', dsn='192.168.240.177:1598/DAPM')
cursor = conn.cursor()
cursor.execute("SELECT TABLE_NAME, STATUS FROM DBA_TABLES WHERE OWNER = 'AGWAPP' AND TABLE_NAME LIKE 'API_STATS%' ORDER BY TABLE_NAME")
for row in cursor.fetchall():
print(f" {row[0]} ({row[1]})")
cursor.close()
conn.close()
-3
View File
@@ -2,6 +2,3 @@ org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryEr
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
org.gradle.configureondemand=true org.gradle.configureondemand=true
reposiliteUser=admin
reposilitePassword=BFoZsHrQU4lmJf9rruDdcsPgE0gltKfRdTjAx04IgvFhrd5q07QwMFZqeLHFv+5n
+12 -12
View File
@@ -1,4 +1,4 @@
CREATE TABLE EMSADM.API_HISTORY CREATE TABLE EMSAPP.API_HISTORY
( (
API_ID VARCHAR2(30), API_ID VARCHAR2(30),
SEQ NUMBER(22), SEQ NUMBER(22),
@@ -9,23 +9,23 @@ CREATE TABLE EMSADM.API_HISTORY
LOB (SAVEDDATA) STORE AS SECUREFILE (TABLESPACE TS_AGW_D01) LOB (SAVEDDATA) STORE AS SECUREFILE (TABLESPACE TS_AGW_D01)
TABLESPACE TS_EMS_D01; TABLESPACE TS_EMS_D01;
COMMENT ON TABLE EMSADM.API_HISTORY IS 'API 변경이력 테이블'; COMMENT ON TABLE EMSAPP.API_HISTORY IS 'API 변경이력 테이블';
COMMENT ON COLUMN EMSADM.API_HISTORY.API_ID IS 'API ID(TSEAIHE01.EAISVCNAME)'; COMMENT ON COLUMN EMSAPP.API_HISTORY.API_ID IS 'API ID(TSEAIHE01.EAISVCNAME)';
COMMENT ON COLUMN EMSADM.API_HISTORY.SEQ IS 'Sequence 값'; COMMENT ON COLUMN EMSAPP.API_HISTORY.SEQ IS 'Sequence 값';
COMMENT ON COLUMN EMSADM.API_HISTORY.SAVEDDATA IS '변경저장 정보'; COMMENT ON COLUMN EMSAPP.API_HISTORY.SAVEDDATA IS '변경저장 정보';
COMMENT ON COLUMN EMSADM.API_HISTORY.USERID IS '사용자ID'; COMMENT ON COLUMN EMSAPP.API_HISTORY.USERID IS '사용자ID';
COMMENT ON COLUMN EMSADM.API_HISTORY.SAVEDDT IS '저장일시'; COMMENT ON COLUMN EMSAPP.API_HISTORY.SAVEDDT IS '저장일시';
CREATE UNIQUE INDEX EMSADM.PK_API_HISTORY ON EMSADM.API_HISTORY CREATE UNIQUE INDEX EMSAPP.PK_API_HISTORY ON EMSAPP.API_HISTORY
(API_ID, SEQ) (API_ID, SEQ)
TABLESPACE TS_EMS_I01; TABLESPACE TS_EMS_I01;
ALTER TABLE EMSADM.API_HISTORY ADD ( ALTER TABLE EMSAPP.API_HISTORY ADD (
CONSTRAINT PK_API_HISTORY CONSTRAINT PK_API_HISTORY
PRIMARY KEY PRIMARY KEY
(API_ID, SEQ) (API_ID, SEQ)
USING INDEX EMSADM.PK_API_HISTORY USING INDEX EMSAPP.PK_API_HISTORY
ENABLE VALIDATE); ENABLE VALIDATE);
GRANT DELETE, INSERT, SELECT, UPDATE ON EMSADM.API_HISTORY TO RL_EMS_ALL; GRANT DELETE, INSERT, SELECT, UPDATE ON EMSAPP.API_HISTORY TO RL_EMS_ALL;
GRANT SELECT ON EMSADM.API_HISTORY TO RL_EMS_SEL; GRANT SELECT ON EMSAPP.API_HISTORY TO RL_EMS_SEL;
+30 -30
View File
@@ -39,37 +39,37 @@
```sql ```sql
-- 개발 서버용 -- 개발 서버용
insert into EMSADM.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.url', 'http://172.31.32.111:10230/BATAppWeb/EaiBatCall');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.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.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSADM.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.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.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.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.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.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
insert into EMSADM.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.host_url', 'http://172.31.35.144:9021');
insert into EMSADM.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.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
-- autogen template -- autogen template
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.url', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.url', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', '');
insert into EMSADM.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.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
``` ```
+4 -4
View File
@@ -163,13 +163,13 @@ SMS 인증번호 발송 - authCode: 123456
```sql ```sql
-- SMS 인증 설정 초기화 -- SMS 인증 설정 초기화
INSERT INTO EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) INSERT INTO EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
VALUES ('Monitoring', 'kjb.sms_auth.enabled', 'true'); VALUES ('Monitoring', 'kjb.sms_auth.enabled', 'true');
INSERT INTO EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) INSERT INTO EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
VALUES ('Monitoring', 'kjb.sms_auth.mode', 'real'); VALUES ('Monitoring', 'kjb.sms_auth.mode', 'real');
INSERT INTO EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) INSERT INTO EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
VALUES ('Monitoring', 'kjb.sms_auth.fixed_value', '654321'); VALUES ('Monitoring', 'kjb.sms_auth.fixed_value', '654321');
``` ```
@@ -187,7 +187,7 @@ SMS 발송을 위해 UMS 관련 프로퍼티가 설정되어 있어야 합니다
### Q: SMS 인증을 일시적으로 비활성화하려면? ### Q: SMS 인증을 일시적으로 비활성화하려면?
```sql ```sql
UPDATE EMSADM.TSEAIRM24 UPDATE EMSAPP.TSEAIRM24
SET PRPTY2VAL = 'false' SET PRPTY2VAL = 'false'
WHERE PRPTYGROUPNAME = 'Monitoring' AND PRPTYNAME = 'kjb.sms_auth.enabled'; WHERE PRPTYGROUPNAME = 'Monitoring' AND PRPTYNAME = 'kjb.sms_auth.enabled';
``` ```
+65 -65
View File
@@ -428,7 +428,7 @@ WebContent/
``` ```
-Deai.datasource.type=DEV -Deai.datasource.type=DEV
-Dinst.Name=emsSvr11 -Dinst.Name=emsSvr11
-Deai.tableowner=EMSADM -Deai.tableowner=EMSAPP
-Deai.systemmode=D -Deai.systemmode=D
-Dfile.encoding=utf-8 -Dfile.encoding=utf-8
-DLOGBACK_LOG_LEVEL=info -DLOGBACK_LOG_LEVEL=info
@@ -438,7 +438,7 @@ WebContent/
``` ```
-Deai.datasource.type=DEV -Deai.datasource.type=DEV
-Dinst.Name=emsSvr99 -Dinst.Name=emsSvr99
-Deai.tableowner=EMSADM -Deai.tableowner=EMSAPP
-Deai.systemmode=D -Deai.systemmode=D
-Dfile.encoding=utf-8 -Dfile.encoding=utf-8
-Dlogin.mode=db -Dlogin.mode=db
@@ -577,82 +577,82 @@ gradle test --tests "com.eactive.eai.rms.*"
```sql ```sql
-- 로컬 개발용 (외부) -- 로컬 개발용 (외부)
-- EAI, UMS 주소 미기입시 내부적으론 호출 시도 하지 않고 warning 로그만 남기며 정상 처리 -- EAI, UMS 주소 미기입시 내부적으론 호출 시도 하지 않고 warning 로그만 남기며 정상 처리
--insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', ''); --insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.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.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSADM.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.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.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.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.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.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
-- insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', ''); -- insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', '');
insert into EMSADM.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.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
--insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', ''); --insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI='); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSADM.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_uri', '/api/billing/findBillingForCondition');
insert into EMSADM.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.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSADM.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.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
commit; commit;
-- 은행 개발 서버용 -- 은행 개발 서버용
insert into EMSADM.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.url', 'http://172.31.32.111:10230/BATAppWeb/EaiBatCall');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.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.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSADM.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.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.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.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.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.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
insert into EMSADM.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.host_url', 'http://172.31.35.144:9021');
insert into EMSADM.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.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
insert into EMSADM.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.host_url', 'http://192.168.246.15:8181');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI='); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSADM.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_uri', '/api/billing/findBillingForCondition');
insert into EMSADM.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.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSADM.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.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
commit; commit;
-- 은행 운영 서버용 -- 은행 운영 서버용
insert into EMSADM.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.url', 'http://172.21.1.40:10860/BATAppWeb/EaiBatCall');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.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.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSADM.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.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.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.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.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.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
insert into EMSADM.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.host_url', 'http://172.24.128.66:9021');
insert into EMSADM.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.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
insert into EMSADM.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.host_url', 'http://192.168.246.15:8181');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI='); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSADM.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_uri', '/api/billing/findBillingForCondition');
insert into EMSADM.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.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSADM.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.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
commit; commit;
``` ```
## 라이선스 ## 라이선스
@@ -252,6 +252,18 @@ public interface MonitoringContext {
public static final String KEYSTORE_UPLOAD_PATH = "keystore.upload.path"; public static final String KEYSTORE_UPLOAD_PATH = "keystore.upload.path";
// 이중 로그인 허용여부
public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED";
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 10)
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
// 웹훅 재전송 설정
public static final String API_WEBHOOK_RETRY_COUNT = "api.webhook.retry_count";
public static final String API_WEBHOOK_RETRY_TIME = "api.webhook.retry_time";
public abstract String getStringProperty(String propertyName); public abstract String getStringProperty(String propertyName);
public abstract String getStringProperty(String propertyName, String defaultString); public abstract String getStringProperty(String propertyName, String defaultString);
@@ -1,15 +1,18 @@
package com.eactive.eai.rms.common.interceptor; package com.eactive.eai.rms.common.interceptor;
import com.eactive.eai.rms.common.login.LoginVo; import javax.servlet.http.HttpServletRequest;
import com.eactive.eai.rms.common.login.SessionManager; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ClassUtils; import org.apache.commons.lang3.ClassUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest; import com.eactive.eai.rms.common.context.MonitoringContext;
import javax.servlet.http.HttpServletResponse; import com.eactive.eai.rms.common.login.LoginVo;
import com.eactive.eai.rms.common.login.SessionManager;
public class SessionCheckInterceptor extends HandlerInterceptorAdapter { public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
@@ -20,6 +23,8 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = Logger private static final Logger logger = Logger
.getLogger(SessionCheckInterceptor.class); .getLogger(SessionCheckInterceptor.class);
@Autowired
private MonitoringContext monitoringContext;
@Override @Override
public boolean preHandle(HttpServletRequest request, public boolean preHandle(HttpServletRequest request,
@@ -39,6 +44,7 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
//craft //craft
boolean isSkippable = ClassUtils.isAssignable(clazz, InterceptorSkipController.class); boolean isSkippable = ClassUtils.isAssignable(clazz, InterceptorSkipController.class);
boolean isDualLogin = monitoringContext.getBooleanProperty(MonitoringContext.RMS_DUAL_LOGIN_ENABLED, true);
if (isSkippable) { if (isSkippable) {
valid = true; valid = true;
@@ -46,7 +52,7 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
SessionManager.setLoginVo(loginVo); // ThreadLocal에 설정 SessionManager.setLoginVo(loginVo); // ThreadLocal에 설정
// 이중 로그인 검사 // 이중 로그인 검사
if (!SessionManager.isValidSession(request)) { if (!SessionManager.isValidSession(request, isDualLogin)) {
logger.warn("Duplicate login detected for user: " + loginVo.getUserId()); logger.warn("Duplicate login detected for user: " + loginVo.getUserId());
valid = false; valid = false;
} else { } else {
@@ -391,6 +391,9 @@ public class MainController implements InterceptorSkipController {
session.setAttribute("dualLogin", "N"); session.setAttribute("dualLogin", "N");
boolean isDualLogin = monitoringContext.getBooleanProperty(MonitoringContext.RMS_DUAL_LOGIN_ENABLED, true);
if (!isDualLogin) {
// 이중 로그인 확인 // 이중 로그인 확인
boolean isNewLogin = SessionManager.registerUserSession(request, dto); boolean isNewLogin = SessionManager.registerUserSession(request, dto);
@@ -405,6 +408,7 @@ public class MainController implements InterceptorSkipController {
// model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다."); // model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다.");
// return "/"; // return "/";
} }
}
// 세션에 로그인 정보 저장 // 세션에 로그인 정보 저장
request.getSession().setAttribute(CommonConstants.LOGIN, dto); request.getSession().setAttribute(CommonConstants.LOGIN, dto);
@@ -613,6 +617,8 @@ public class MainController implements InterceptorSkipController {
menuId = menuId == null ? "" : menuId; menuId = menuId == null ? "" : menuId;
model.addAttribute("mainPage", mainPage); model.addAttribute("mainPage", mainPage);
model.addAttribute("menuId", menuId); model.addAttribute("menuId", menuId);
model.addAttribute("autoLogoutTimeout",
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 10));
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("CURRENT SERVICE TYPE : " + service); logger.debug("CURRENT SERVICE TYPE : " + service);
@@ -665,7 +671,8 @@ public class MainController implements InterceptorSkipController {
if (loginVo != null) { if (loginVo != null) {
// 로그인 세션 제거 // 로그인 세션 제거
SessionManager.removeUserSession(loginVo.getUserId()); //SessionManager.removeUserSession(loginVo.getUserId());
SessionManager.removeUserSessionBySessionId(request.getSession().getId()); //2026.04.29 이중로그인 로그아웃 버그 수정
request.getSession().removeAttribute(CommonConstants.LOGIN); request.getSession().removeAttribute(CommonConstants.LOGIN);
@@ -27,6 +27,7 @@ public class SessionDestructionListener implements HttpSessionListener {
HttpSession session = se.getSession(); HttpSession session = se.getSession();
String userId = (String) session.getAttribute("userId"); String userId = (String) session.getAttribute("userId");
String sessionId = session.getId();
// 로그인 한 사용자의 세션정보 정리 // 로그인 한 사용자의 세션정보 정리
if (userId != null) { if (userId != null) {
@@ -34,7 +35,8 @@ public class SessionDestructionListener implements HttpSessionListener {
logger.debug("[SessionListener] 세션만료 감지. 사용자 제거: " + userId); logger.debug("[SessionListener] 세션만료 감지. 사용자 제거: " + userId);
} }
SessionManager.removeUserSession(userId); //SessionManager.removeUserSession(userId);
SessionManager.removeUserSessionBySessionId(sessionId); //2026.04.29 이중로그인 로그아웃 버그 수정
} }
} catch (Exception e) { } catch (Exception e) {
@@ -4,6 +4,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.util.CommonConstants; import com.eactive.eai.rms.common.util.CommonConstants;
//craft //craft
@@ -206,17 +207,29 @@ public class SessionManager {
} }
} }
/**
* 사용자의 로그인 세션을 제거 (이중로그인 버그 수정용)
* @param sessionId 로그인한 session ID
*/
public static void removeUserSessionBySessionId(String sessionId) {
loggedInUsers.entrySet().removeIf(entry -> sessionId.equals(entry.getValue()));
}
/** /**
* 현재 세션이 유효한 로그인 세션인지 확인 * 현재 세션이 유효한 로그인 세션인지 확인
* @param request HTTP 요청 * @param request HTTP 요청
* @param isDualLogin 이중 로그인 허용여부
* @return true: 유효한 세션, false: 유효하지 않은 세션 * @return true: 유효한 세션, false: 유효하지 않은 세션
*/ */
public static boolean isValidSession(HttpServletRequest request) { public static boolean isValidSession(HttpServletRequest request, boolean isDualLogin) {
LoginVo loginVo = getLoginVo(request); LoginVo loginVo = getLoginVo(request);
if (loginVo == null) { if (loginVo == null) {
return false; return false;
} }
if (isDualLogin) {
return true;
} else {
String userId = loginVo.getUserId(); String userId = loginVo.getUserId();
String currentSessionId = request.getSession().getId(); String currentSessionId = request.getSession().getId();
String registeredSessionId = loggedInUsers.get(userId); String registeredSessionId = loggedInUsers.get(userId);
@@ -224,5 +237,8 @@ public class SessionManager {
// 등록된 세션이 없거나 현재 세션과 다른 경우 // 등록된 세션이 없거나 현재 세션과 다른 경우
return registeredSessionId != null && registeredSessionId.equals(currentSessionId); return registeredSessionId != null && registeredSessionId.equals(currentSessionId);
} }
}
} }
@@ -1,13 +1,11 @@
package com.eactive.eai.rms.common.scheduler; package com.eactive.eai.rms.common.scheduler;
import com.eactive.eai.rms.common.base.BaseService; import java.text.SimpleDateFormat;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper; import java.util.Date;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI; import java.util.List;
import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo; import java.util.stream.Collectors;
import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch; import java.util.stream.StreamSupport;
import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.quartz.JobDetail; import org.quartz.JobDetail;
import org.quartz.JobKey; import org.quartz.JobKey;
@@ -20,9 +18,14 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.List; import com.eactive.eai.rms.common.base.BaseService;
import java.util.stream.Collectors; import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper;
import java.util.stream.StreamSupport; import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI;
import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo;
import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch;
import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService;
@Service("schedulerService") @Service("schedulerService")
@Transactional(transactionManager = "transactionManagerForEMS") @Transactional(transactionManager = "transactionManagerForEMS")
@@ -122,10 +125,10 @@ public class SchedulerManService extends BaseService {
List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(new JobKey(jobName, Scheduler.DEFAULT_GROUP)); List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(new JobKey(jobName, Scheduler.DEFAULT_GROUP));
if(triggersOfJob.size() > 0) { if(triggersOfJob.size() > 0) {
result.setStartTime(String.valueOf(triggersOfJob.get(0).getStartTime())); result.setStartTime(dateFormat(triggersOfJob.get(0).getStartTime()));
result.setEndTime(String.valueOf(triggersOfJob.get(0).getEndTime())); result.setEndTime(dateFormat(triggersOfJob.get(0).getEndTime()));
result.setPreviousFireTime(String.valueOf(triggersOfJob.get(0).getPreviousFireTime())); result.setPreviousFireTime(dateFormat(triggersOfJob.get(0).getPreviousFireTime()));
result.setNextFireTime(String.valueOf(triggersOfJob.get(0).getNextFireTime())); result.setNextFireTime(dateFormat(triggersOfJob.get(0).getNextFireTime()));
} }
} catch (Exception e) { } catch (Exception e) {
@@ -151,4 +154,13 @@ public class SchedulerManService extends BaseService {
jobInfoService.deleteById(jobName); jobInfoService.deleteById(jobName);
} }
private String dateFormat(Date date) {
if (date == null) {
return "";
} else {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
} }
@@ -1,5 +1,6 @@
package com.eactive.eai.rms.common.util; package com.eactive.eai.rms.common.util;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@@ -191,6 +192,20 @@ public class StringUtils
} }
return sb.toString(); return sb.toString();
} }
public static String toString(Object val) {
return val != null ? val.toString() : null;
}
public static Long toLong(Object val) {
return val != null ? ((Number) val).longValue() : null;
}
public static BigDecimal toDecimal(Object val) {
if (val == null) return null;
if (val instanceof BigDecimal) return (BigDecimal) val;
return new BigDecimal(val.toString());
}
// //
// /** // /**
// * - "" 나 Null 을 입력받아 SPACE 로 변환한다. // * - "" 나 Null 을 입력받아 SPACE 로 변환한다.
@@ -107,6 +107,17 @@ public class UserInfoService extends
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
public List<UserInfo> findAllByRoleId(String roleId) {
QUserInfo qUserInfo = QUserInfo.userInfo;
QUserRole qUserRole = QUserRole.userRole;
return getJPAQueryFactory()
.selectFrom(qUserInfo)
.join(qUserRole).on(qUserInfo.userid.eq(qUserRole.id.userId))
.where(qUserRole.id.roleId.eq(roleId))
.fetch();
}
public Page<UserInfo> findByUsername(Pageable pageable, String username) { public Page<UserInfo> findByUsername(Pageable pageable, String username) {
QUserInfo qUserInfo = QUserInfo.userInfo; QUserInfo qUserInfo = QUserInfo.userInfo;
BooleanExpression predicate = qUserInfo.isNotNull(); BooleanExpression predicate = qUserInfo.isNotNull();
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@EMSDataSource
public interface PortalInquiryCommentRepository extends BaseRepository<InquiryComment, String> {
long countByInquiry_Id(String inquiryId);
long countByInquiry_IdAndDelYn(String inquiryId, String delYn);
List<InquiryComment> findByInquiry_IdAndDelYnOrderByCreatedDateAsc(String inquiryId, String delYn);
Optional<InquiryComment> findTopByInquiry_IdAndDelYnOrderByCreatedDateDesc(
String inquiryId, String delYn);
Optional<InquiryComment> findTopByInquiry_IdAndAdminYnAndDelYnOrderByCreatedDateDesc(
String inquiryId, String adminYn, String delYn);
boolean existsByInquiry_IdAndAdminYnAndDelYnAndCreatedDateAfter(
String inquiryId, String adminYn, String delYn, LocalDateTime after);
}
@@ -0,0 +1,31 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalInquiryCommentService extends AbstractEMSDataSerivce<InquiryComment, String, PortalInquiryCommentRepository> {
// AbstractEMSDataSerivce의 repository 필드가 BaseRepository<T,I>로 고정되어
// 커스텀 메서드를 호출할 수 없으므로 별도 필드로 직접 주입
@Autowired
private PortalInquiryCommentRepository inquiryCommentRepository;
public long countByInquiryId(String inquiryId) {
return inquiryCommentRepository.countByInquiry_Id(inquiryId);
}
public long countActiveByInquiryId(String inquiryId) {
return inquiryCommentRepository.countByInquiry_IdAndDelYn(inquiryId, "N");
}
public List<InquiryComment> findByInquiryId(String inquiryId) {
return inquiryCommentRepository.findByInquiry_IdAndDelYnOrderByCreatedDateAsc(inquiryId, "N");
}
}
@@ -4,7 +4,10 @@ import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import java.util.List;
@EMSDataSource @EMSDataSource
public interface PortalInquiryRepository extends BaseRepository<Inquiry, String> { public interface PortalInquiryRepository extends BaseRepository<Inquiry, String> {
List<Inquiry> findByInquiryStatus(String inquiryStatus);
} }
@@ -1,20 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry; package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.qna.entity.Inquiry; import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.apim.portal.qna.entity.QInquiry; import com.eactive.apim.portal.qna.entity.QInquiry;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce; import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service @Service
@Transactional(transactionManager = "transactionManagerForEMS") @Transactional(transactionManager = "transactionManagerForEMS")
public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String, PortalInquiryRepository> { public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String, PortalInquiryRepository> {
// AbstractEMSDataSerivce의 repository 필드가 BaseRepository<T,I>로 고정되어
// 커스텀 메서드를 호출할 수 없으므로 별도 필드로 직접 주입
@Autowired
private PortalInquiryRepository portalInquiryRepository;
public Page<Inquiry> findAll(Pageable pageable, PortalInquirySearch portalInquirySearch) { public Page<Inquiry> findAll(Pageable pageable, PortalInquirySearch portalInquirySearch) {
QInquiry qInquiry = QInquiry.inquiry; QInquiry qInquiry = QInquiry.inquiry;
@@ -58,4 +66,8 @@ public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String
repository.deleteAll(inquiries); repository.deleteAll(inquiries);
} }
} }
public List<Inquiry> findByInquiryStatus(String inquiryStatus) {
return portalInquiryRepository.findByInquiryStatus(inquiryStatus);
}
} }
@@ -10,6 +10,9 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@Service @Service
@Transactional(transactionManager = "transactionManagerForEMS") @Transactional(transactionManager = "transactionManagerForEMS")
public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> { public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> {
@@ -19,6 +22,10 @@ public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, St
BooleanBuilder predicate = new BooleanBuilder(); BooleanBuilder predicate = new BooleanBuilder();
if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchNoticeType())) {
predicate.and(qPortalNotice.noticeType.eq(portalNoticeUISearch.getSearchNoticeType()));
}
if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchUseYn())) { if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchUseYn())) {
predicate.and(qPortalNotice.useYn.eq(portalNoticeUISearch.getSearchUseYn())); predicate.and(qPortalNotice.useYn.eq(portalNoticeUISearch.getSearchUseYn()));
} }
@@ -28,7 +35,12 @@ public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, St
.or(qPortalNotice.noticeDetail.containsIgnoreCase(portalNoticeUISearch.getSearchSubjectDetail()))); .or(qPortalNotice.noticeDetail.containsIgnoreCase(portalNoticeUISearch.getSearchSubjectDetail())));
} }
return repository.findAll(predicate, pageable); //return repository.findAll(predicate, pageable);
Sort fixYnFirst = Sort.by(Sort.Direction.DESC, "fixYn");
Sort combinedSort = fixYnFirst.and(pageable.getSort());
Pageable fixedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), combinedSort);
return repository.findAll(predicate, fixedPageable);
} }
} }
@@ -5,6 +5,7 @@ import lombok.Data;
@Data @Data
public class PortalNoticeUISearch { public class PortalNoticeUISearch {
private String searchNoticeType;
private String searchUseYn; private String searchUseYn;
private String searchSubjectDetail; private String searchSubjectDetail;
@@ -0,0 +1,42 @@
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import com.eactive.eai.data.entity.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@Entity
@Table(name = "API_STATUS")
public class ApiStatus extends AbstractEntity<String> {
@Id
@Column(name = "EAISVCNAME", length = 100)
private String eaisvcname;
@Column(name = "STATUS_CODE", length = 1)
private String statusCode;
@LastModifiedBy // ← EMSAuditorAware가 자동으로 채움
@Column(name = "MODIFIED_BY", length = 50)
private String modifiedBy = "SCHEDULER";
@LastModifiedDate // ← save() 시 자동으로 sysdate 채움
@Column(name = "MODIFIED_DATE")
private LocalDateTime modifiedDate;
@Override
public @NonNull String getId() { return eaisvcname; }
}
@@ -0,0 +1,8 @@
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
public interface ApiStatusEvent {
String getEaisvcname();
String getEaisvcdesc();
String getPrevStatusCode();
String getEvent();
}
@@ -0,0 +1,7 @@
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
public interface InflowTokenInsufficient {
String getEaisvcname();
String getEaisvcdesc();
Long getCnt();
}
@@ -0,0 +1,47 @@
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import com.eactive.eai.data.entity.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@Entity
@Table(name = "TSEAIFR11")
public class InflowTokenInsufficientLog extends AbstractEntity<InflowTokenInsufficientLogId> implements Serializable {
private static final long serialVersionUID = 1L;
@EmbeddedId
@EqualsAndHashCode.Include
private InflowTokenInsufficientLogId id;
@Column(name = "EAISEVRINSTNCNAME", length = 20)
private String eaisevrinstncname;
@Column(name = "EAISVCNAME", length = 30)
private String eaisvcname;
@Column(name = "ADPTRBZWKGROUPNAME", length = 50)
private String adptrbzwkgroupname;
@Column(name = "THRESHOLDPERSECOND")
private Long thresholdpersecond;
@Column(name = "THRESHOLD")
private Long threshold;
@Column(name = "THRESHOLDTIMEUNIT", length = 12)
private String thresholdtimeunit;
@Override
public @NonNull InflowTokenInsufficientLogId getId() { return id; }
}
@@ -0,0 +1,21 @@
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import lombok.Data;
@Data
@Embeddable
public class InflowTokenInsufficientLogId implements Serializable {
private static final long serialVersionUID = 1L;
@Column(name = "EAIBZWKDSTCD", length = 4)
private String eaibzwkdstcd;
@Column(name = "MSGDPSTYMS", length = 17)
private String msgdpstyms;
@Column(name = "EAISVCSERNO", length = 41)
private String eaisvcserno;
}
@@ -0,0 +1,33 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "PTL_WEBHOOK_REQ")
@Getter
@NoArgsConstructor
public class WebhookReq {
@Id
@Column(name = "ID", length = 36, nullable = false)
private String id;
@Column(name = "ORG_ID", length = 100)
@Comment("법인 ID")
private String orgId;
@Column(name = "TARGET_URL", length = 500, nullable = false)
@Comment("웹훅 수신 URL")
private String targetUrl;
@Column(name = "SECRET", length = 200)
@Comment("HMAC 서명용 Secret Key")
private String secret;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_API")
@Getter
@NoArgsConstructor
public class WebhookReqApi implements Serializable {
@EmbeddedId
private WebhookReqApiId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqApiId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "API_ID", nullable = false, length = 100)
@Comment("API ID")
private String apiId;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_EVENT")
@Getter
@NoArgsConstructor
public class WebhookReqEvent implements Serializable {
@EmbeddedId
private WebhookReqEventId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqEventId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "EVENT_TYPE", nullable = false, length = 50)
@Comment("이벤트 유형 (CONTROL_START, ERROR_START 등)")
private String eventType;
}
@@ -0,0 +1,77 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "PTL_WEBHOOK_SEND_LOG")
@Getter
@Setter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class WebhookSendLog {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "webhook_send_log_seq")
@SequenceGenerator(
name = "webhook_send_log_seq",
sequenceName = "SEQ_PTL_WEBHOOK_SEND_LOG",
allocationSize = 1
)
private Long id;
@Column(name = "TARGET_URL", nullable = false, length = 500)
private String targetUrl;
@Column(name = "EVENT_TYPE", length = 100)
private String eventType;
@Lob
@Column(name = "PAYLOAD")
private String payload;
@Column(name = "SIGNATURE", length = 500)
private String signature;
@Column(name = "STATUS_CODE")
private Integer statusCode;
@Column(name = "ORG_ID")
private String orgId;
@Lob
@Column(name = "RESPONSE_BODY")
private String responseBody;
@Column(name = "SUCCESS", length = 1)
private String success; // 오라클 CHAR(1) : 'Y' / 'N'
@Lob
@Column(name = "ERROR_MESSAGE")
private String errorMessage;
@Column(name = "RETRY_COUNT")
private Integer retryCount = 0;
@CreatedDate
@Column(name = "CREATED_AT", updatable = false)
private LocalDateTime createdAt;
@Column(name = "SENT_AT")
private LocalDateTime sentAt;
/* Boolean 편의 메서드 */
public void setSuccess(Boolean success) {
this.success = (success != null && success) ? "Y" : "N";
}
public Boolean getSuccess() {
return "Y".equalsIgnoreCase(this.success);
}
}
@@ -13,7 +13,9 @@ import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.eactive.apim.portal.app.entity.QCredential;
import com.eactive.eai.data.entity.onl.adapter.QAdapterGroup; import com.eactive.eai.data.entity.onl.adapter.QAdapterGroup;
import com.eactive.eai.data.entity.onl.authserver.QClientEntity;
import com.eactive.eai.data.entity.onl.inflow.InflowControl; import com.eactive.eai.data.entity.onl.inflow.InflowControl;
import com.eactive.eai.data.entity.onl.inflow.InflowControlId; import com.eactive.eai.data.entity.onl.inflow.InflowControlId;
import com.eactive.eai.data.entity.onl.inflow.QInflowControl; import com.eactive.eai.data.entity.onl.inflow.QInflowControl;
@@ -25,7 +27,6 @@ import com.eactive.eai.rms.onl.manage.inflow.history.InflowControlHistoryManUISe
import com.eactive.eai.rms.onl.manage.inflow.inflow.InflowControlManMapper; import com.eactive.eai.rms.onl.manage.inflow.inflow.InflowControlManMapper;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple; import com.querydsl.core.Tuple;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
@@ -33,10 +34,15 @@ import com.querydsl.jpa.impl.JPAQueryFactory;
public class InflowControlService extends AbstractDataService<InflowControl, InflowControlId, InflowControlRepository> { public class InflowControlService extends AbstractDataService<InflowControl, InflowControlId, InflowControlRepository> {
private static final String ADAPTER_TYPE_INFLOW = "01"; private static final String ADAPTER_TYPE_INFLOW = "01";
private static final String INTERFACE_TYPE_INFLOW = "02";
private static final String CLIENT_TYPE_INFLOW = "03";
@PersistenceContext @PersistenceContext
EntityManager entityManager; EntityManager entityManager;
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
EntityManager entityManagerForEMS;
@Autowired @Autowired
InflowControlManMapper mapper; InflowControlManMapper mapper;
@@ -124,7 +130,7 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc) .select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
.from(qEAIMessageEntity) .from(qEAIMessageEntity)
.leftJoin(qInflowControl) .leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq("02").and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname))) .on(qInflowControl.id.type.eq(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
.where(qEAIMessageEntity.eaisvcname.contains(searchName)) .where(qEAIMessageEntity.eaisvcname.contains(searchName))
.orderBy(qEAIMessageEntity.eaisvcname.asc()) .orderBy(qEAIMessageEntity.eaisvcname.asc())
.offset(pageable.getOffset()) .offset(pageable.getOffset())
@@ -144,6 +150,8 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
return new PageImpl<>(dtoList, pageable, totalCount); return new PageImpl<>(dtoList, pageable, totalCount);
} }
private InflowControlServiceDto toDto(QEAIMessageEntity qEAIMessageEntity, QInflowControl qInflowControl, private InflowControlServiceDto toDto(QEAIMessageEntity qEAIMessageEntity, QInflowControl qInflowControl,
Tuple tuple) { Tuple tuple) {
InflowControlServiceDto dto = null; InflowControlServiceDto dto = null;
@@ -167,13 +175,75 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc) .select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
.from(qEAIMessageEntity) .from(qEAIMessageEntity)
.leftJoin(qInflowControl) .leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq("02").and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname))) .on(qInflowControl.id.type.eq(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
.where(qEAIMessageEntity.eaisvcname.eq(eaisvcname)) .where(qEAIMessageEntity.eaisvcname.eq(eaisvcname))
.fetchOne(); .fetchOne();
return toDto(qEAIMessageEntity, qInflowControl, tuple); return toDto(qEAIMessageEntity, qInflowControl, tuple);
} }
public InflowControlServiceDto findByIdForClient(String clientId) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
Tuple tuple = jpaQueryFactory
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
.from(qClientEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientid.eq(clientId))
.fetchOne();
return toDto(qClientEntity, qInflowControl, tuple);
}
public Page<InflowControlServiceDto> findAllForClient(Pageable pageable, String searchName) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
List<Tuple> tupleList = jpaQueryFactory
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
.from(qClientEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientname.contains(searchName))
.orderBy(qClientEntity.clientname.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<InflowControlServiceDto> dtoList = tupleList
.stream()
.map(tuple -> toDto(qClientEntity, qInflowControl, tuple))
.collect(Collectors.toList());
long totalCount = jpaQueryFactory
.select(qClientEntity.clientname.count())
.from(qClientEntity)
.where(qClientEntity.clientname.contains(searchName))
.fetchOne();
return new PageImpl<>(dtoList, pageable, totalCount);
}
private InflowControlServiceDto toDto(QClientEntity qClientEntity, QInflowControl qInflowControl,
Tuple tuple) {
InflowControlServiceDto dto = null;
if (tuple.get(qInflowControl) != null)
dto = mapper.toDto(tuple.get(qInflowControl));
else {
dto = new InflowControlServiceDto();
dto.setName(tuple.get(qClientEntity.clientid));
dto.setType(CLIENT_TYPE_INFLOW);
}
dto.setDesc(tuple.get(qClientEntity.clientname));
return dto;
}
public Page<Tuple> selectLogList(Pageable pageable, InflowControlHistoryManUISearch uiSearch) { public Page<Tuple> selectLogList(Pageable pageable, InflowControlHistoryManUISearch uiSearch) {
QInflowControlLog qlog = QInflowControlLog.inflowControlLog; QInflowControlLog qlog = QInflowControlLog.inflowControlLog;
QInflowControlGroup qgroup = QInflowControlGroup.inflowControlGroup; QInflowControlGroup qgroup = QInflowControlGroup.inflowControlGroup;
@@ -120,11 +120,11 @@ public class ApiStatsDayService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt) // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -211,11 +211,10 @@ public class ApiStatsHourService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -293,9 +292,9 @@ public class ApiStatsHourService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -214,11 +214,11 @@ public class ApiStatsMinuteService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt) // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -120,11 +120,11 @@ public class ApiStatsMonthService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt) // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -172,9 +172,9 @@ public class ApiStatsMonthService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -117,11 +117,11 @@ public class ApiStatsYearService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt) // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -169,9 +169,9 @@ public class ApiStatsYearService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -0,0 +1,13 @@
package com.eactive.eai.rms.data.entity.onl.security;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.eactive.eai.data.DataService;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
public interface CryptoModuleConfigDataService extends DataService<CryptoModuleConfig, String> {
Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType, String keySourceType, String useYn);
}
@@ -0,0 +1,38 @@
package com.eactive.eai.rms.data.entity.onl.security;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
import com.eactive.eai.data.entity.onl.security.QCryptoModuleConfig;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.querydsl.core.types.dsl.BooleanExpression;
@Service
@Transactional
public class CryptoModuleConfigDataServiceImpl
extends AbstractDataService<CryptoModuleConfig, String, CryptoModuleConfigRepository>
implements CryptoModuleConfigDataService {
@Override
public Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType,
String keySourceType, String useYn) {
QCryptoModuleConfig q = QCryptoModuleConfig.cryptoModuleConfig;
BooleanExpression predicate = q.cryptoName.containsIgnoreCase(searchName != null ? searchName : "");
if (algType != null && !algType.isEmpty()) {
predicate = predicate.and(q.algType.eq(algType));
}
if (keySourceType != null && !keySourceType.isEmpty()) {
predicate = predicate.and(q.keySourceType.eq(keySourceType));
}
if (useYn != null && !useYn.isEmpty()) {
predicate = predicate.and(q.useYn.eq(useYn));
}
return repository.findAll(predicate, pageable);
}
}
@@ -0,0 +1,10 @@
package com.eactive.eai.rms.data.entity.onl.security;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
import com.eactive.eai.data.jpa.BaseRepository;
interface CryptoModuleConfigRepository
extends BaseRepository<CryptoModuleConfig, String>, QuerydslPredicateExecutor<CryptoModuleConfig> {
}
@@ -26,7 +26,7 @@ public class ExtendedColumnDefinitionService
public List<ExtendedColumnDefinition> findAll() { public List<ExtendedColumnDefinition> findAll() {
QExtendedColumnDefinition q = QExtendedColumnDefinition.extendedColumnDefinition; QExtendedColumnDefinition q = QExtendedColumnDefinition.extendedColumnDefinition;
List<ExtendedColumnDefinition> list = new ArrayList<>(); List<ExtendedColumnDefinition> list = new ArrayList<>();
repository.findAll(q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add); repository.findAll(q.orderSeq.asc(), q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add);
return list; return list;
} }
@@ -0,0 +1,41 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class ApiStatusDetectionService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class);
/**
* 이벤트 감지
* @param event "CONTROL_START" : 점검시작
"CONTROL_END" : 점검종료
"ERROR_START" : 장애시작
"ERROR_END" : 장애종료
"DELAY_START" : 지연시작
"DELAY_END" : 지연종료
* @param apiIds API IDs
*/
public void detect(String event, List<String> apiIds) {
log.debug("이벤트 감지: {} {}", event, apiIds);
}
// API FSM 내 아직 장애로 남아있는 API 목록
public void remainDownApiIds() {
}
// 정상 동작 - detect()에서 일괄 처리 (복구 = 점검종료, 장애종료, 지연종료)
// public void recovered(String event, String[] apiIds) {
//
// }
}
@@ -0,0 +1,77 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
/**
* API 상태 판단. 정상(N), 점검(C), 지연(D), 장애(E)
*
* 1. 정상/지연/장애 -> 점검중 : CONTROL_START
* 2. 점검 -> 점검X : CONTROL_END
* 3. 정상/지연 -> 장애 : ERROR_START
* 4. 장애 -> 장애X : ERROR_END
* 5. 정상 -> 지연 : DELAY_START
* 6. 지연 -> 지연X : DELAY_END
* 7. 기타 : STAY
*/
@Query(nativeQuery = true, value =
" SELECT EAISVCNAME"
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
+ " , PREV_STATUS_CODE "
+ " , EVENT"
+ " 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'"
+ " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'"
+ " WHEN STATUS_CODE = 'E' AND ERR_YN = 'N' THEN 'ERROR_END'"
+ " WHEN STATUS_CODE = 'N' AND DELAY_YN = 'Y' THEN 'DELAY_START'"
+ " WHEN STATUS_CODE = 'D' AND DELAY_YN = 'N' THEN 'DELAY_END'"
+ " ELSE 'STAY' END AS EVENT"
+ " FROM ("
+ " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
//TODO: 게시판 임시로 생성하여 사용. 유차장이 완료하면 변경할것
+ " , NVL(( SELECT 'Y' FROM PTL_NOTICE_API X "
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.PTL_NOTICE Y WHERE X.NOTICE_ID = Y.ID)"
//+ " AND TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI') BETWEEN START_TIME AND END_TIME) "
+ " AND A.EAISVCNAME = X.API_NAME),'N') AS CTRL_YN "
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END"
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
+ " FROM API_STATS_MINUTE"
+ " WHERE A.EAISVCNAME = API_NAME"
+ " AND TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:errorRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS ERR_YN"
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN RESP_SUM / TOTAL > :delayAvgRespTime THEN 'Y'"
+ " ELSE 'N' END"
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
+ " , SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME) AS RESP_SUM"
+ " FROM API_STATS_MINUTE"
+ " WHERE A.EAISVCNAME = API_NAME"
+ " AND TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:delayRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS DELAY_YN"
+ " FROM TSEAIHE01 A LEFT OUTER JOIN API_STATUS B ON A.EAISVCNAME = B.EAISVCNAME"
+ " )"
+ " ) A WHERE EVENT != 'STAY'")
List<ApiStatusEvent> findApiStatusEvents(
@Param("errorRate") int errorRate,
@Param("errorRangeMinute") int errorRangeMinute,
@Param("delayRangeMinute") int delayRangeMinute,
@Param("delayAvgRespTime") int delayAvgRespTime
);
}
@@ -0,0 +1,133 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private UmsManager ums;
@Autowired
private ApiStatusDetectionService apiStatusDectionService;
public void updateApiStatus(HashMap<String, String> param) {
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
Integer.parseInt(param.get("errorRate")),
Integer.parseInt(param.get("errorRangeMinute")),
Integer.parseInt(param.get("delayRangeMinute")),
Integer.parseInt(param.get("delayAvgRespTime"))
);
HashMap<String, List<String>> eventMap = new HashMap<>();
for (ApiStatusEvent event : list) {
String newStatusCode = resolveStatusCode(event.getEvent());
if (newStatusCode == null) continue;
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
//이벤트별로 api분리 저장
List<String> apis = (List<String>)eventMap.get(event.getEvent());
if (apis == null) {
apis = new ArrayList<String>();
apis.add(event.getEaisvcname());
eventMap.put(event.getEvent(), apis);
} else {
apis.add(event.getEaisvcname());
}
}
//이벤트별로 ums, webhook을 발송한다
for (Map.Entry<String, List<String>> entry : eventMap.entrySet()) {
String event = entry.getKey();
List<String> apiIds = entry.getValue();
String message = getSwingChatMessage(event, apiIds);
ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, message);
//제휴사 웹훅 발송
ums.sendWebhook(event, apiIds);
//개발자포탈 API상태모니터링 화면을 위한 처리
apiStatusDectionService.detect(event, apiIds);
}
}
private String resolveStatusCode(String event) {
switch (event) {
case "CONTROL_START": return "C"; //점검
case "CONTROL_END": return "N"; //정상
case "ERROR_START": return "E"; //장애
case "ERROR_END": return "N"; //정상
case "DELAY_START": return "D"; //지연
case "DELAY_END": return "N"; //정상
default:
log.warn("알 수 없는 이벤트: {}", event);
return null;
}
}
private String getSwingChatMessage(String event, List<String> apiIds) {
String message = "";
switch (event) {
case "CONTROL_START":
message = "API 서비스 점검이 시작되었습니다"; //점검
break;
case "CONTROL_END":
message = "API 서비스 점검이 완료되었습니다"; //정상
break;
case "ERROR_START":
message = "API 서비스 장애가 발생하였습니다"; //장애
break;
case "ERROR_END":
message = "API 서비스 장애가 복구되었습니다"; //정상
break;
case "DELAY_START":
message = "API 서비스 지연이 발생하였습니다"; //지연
break;
case "DELAY_END":
message = "API 서비스 지연이 복구되었습니다"; //정상
break;
default:
message = "";
}
return message + "\n- " + String.join(",", apiIds);
}
}
@@ -0,0 +1,21 @@
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);
}
@@ -0,0 +1,39 @@
package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
@Service
@Transactional
public class InflowTokenService {
private static final Logger log = LoggerFactory.getLogger(InflowTokenService.class);
@Autowired
private InflowTokenRepository inflowTokenRepository;
@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();
ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, message);
}
}
}
@@ -0,0 +1,143 @@
package com.eactive.eai.rms.ext.djb.job;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsHour;
/**
* API 로그 테이블을 시간별 통계로 집계하는 배치 작업
* TSEAILGXX → API_STATS_HOUR
* 실행 주기: 매시 00:10 (당일 데이터 집계)
*/
@Component
public class ApiStatsHourlyAggregationJob implements Job {
private static final Logger log = LoggerFactory.getLogger(ApiStatsHourlyAggregationJob.class);
@PersistenceContext
private EntityManager entityManager;
private ApiStatsHourService apiStatsHourService;
@Autowired
public void setApiStatsHourService(ApiStatsHourService apiStatsHourService) {
this.apiStatsHourService = apiStatsHourService;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
ApplicationContext appContext = null;
final ApiStatsHourlyAggregationJob selfJob;
try {
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
selfJob = appContext.getBean(ApiStatsHourlyAggregationJob.class);
} catch (SchedulerException e) {
log.error("applicationContext get module error", e);
return;
}
DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
DataSourceContextHolder.setDataSourceType(dataType);
try {
LocalDate targetDate = LocalDate.now();
if (LocalTime.now().isBefore(LocalTime.of(1, 0))) {
targetDate = targetDate.minusDays(1);
}
selfJob.executeManual(targetDate);
} catch (Exception e) {
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
/**
* 수동 실행 메서드 (날짜 지정)
*/
@Transactional
public int executeManual(LocalDate targetDate) {
long startTime = System.currentTimeMillis();
log.info("=== 시간별 통계 집계 작업 시작 === 대상: {}", targetDate);
String searchDate = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String logTableName = CommonUtil.getLogTable(searchDate, true);
// 1. 기존 데이터 삭제
QApiStatsHour q = QApiStatsHour.apiStatsHour;
long deletedCount = apiStatsHourService.getJPAQueryFactory()
.delete(q)
.where(q.statTime.goe(targetDate.atStartOfDay())
.and(q.statTime.lt(targetDate.plusDays(1).atStartOfDay())))
.execute();
log.info("기존 데이터 삭제: {} 건", deletedCount);
// 2. 집계 및 데이터 입력
/**
* EAISVCSERNO로 그룹핑하여 1행으로 집계를 한 다음 (한거래 안에서 adapter가 여러개 있을 경우 seq=100의 adapter를 기준)
* 총건수 : EAISVCSERNO로 그룹핑한 총 건수
* 성공건수 : seq = 400 and EAIERRCD = null 인 건수
* Timeout : EAIERRCD in (공통코드 CODEGROUP = 'ERRCODE_TIMEOUT') 인 건수
* 시스템오류 : (seq400 = null or EAIERRCD is not null) and 공통코드(ERRCODE_TIMEOUT)에 정의되지 않은 errorcode 인 건수
*/
String sql =
"INSERT INTO API_STATS_HOUR" +
" (STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
", TOTAL_CNT, SUCCESS_CNT, TIMEOUT_CNT, SYSTEM_ERR_CNT, BIZ_ERR_CNT" +
", SEQ900_TIMEOUT_CNT, SEQ900_SYSTEM_ERR_CNT, SEQ900_BIZ_ERR_CNT" +
", AVG_RESP_TIME, MIN_RESP_TIME, MAX_RESP_TIME, P50_RESP_TIME, P95_RESP_TIME)" +
" SELECT TO_TIMESTAMP(SUBSTR(DT,1,10) || '0000', 'YYYYMMDDHH24MISS')" +
" , 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 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)" +
" , 0, 0, 0, 0" +
" , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" +
" FROM (" +
" SELECT EAISVCSERNO" +
" , MAX(EAISVCNAME) AS API_NAME, MAX(EAISEVRINSTNCNAME) AS GW_INSTANCE_ID" +
" , MAX(EAIBZWKDSTCD) AS BIZ_DIV_CODE, MAX(CLIENTID) AS CLIENT_ID" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN GSTATSYSADPTRBZWKGROUPNAME ELSE '' END) AS INBOUND_ADAPTER" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN PSVSYSADPTRBZWKGROUPNAME ELSE '' END) AS OUTBOUND_ADAPTER" +
" , MIN(MSGDPSTYMS) AS DT" +
" , MAX(MSGPRCSSYMS) - MIN(MSGDPSTYMS) AS RESP_TIME" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" +
" , MAX(EAIERRCD) AS ERROR_CODE" +
" FROM " + logTableName +
" WHERE MSGDPSTYMS LIKE :searchDate || '%'" +
" GROUP BY EAISVCSERNO" +
" ) A LEFT OUTER JOIN TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.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";
int savedCount = entityManager.createNativeQuery(sql)
.setParameter("searchDate", searchDate)
.executeUpdate();
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("=== 집계 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime);
return savedCount;
}
}
@@ -0,0 +1,175 @@
package com.eactive.eai.rms.ext.djb.job;
import java.util.HashMap;
import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.ext.djb.apistatus.ApiStatusService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
/**
* Job - Quartz Job
* API_STATS_MINUTE 데이터를 1분마다 조회하여 Api 상태를 판단하여 API_STATUS 테이블을 insert/update 한다.
* & api 상태가 변경된 경우, 알림 테이블에 저장한다
*
* <p>Cron Schedule 권장:</p>
* <ul>
* <li>기본: 0 * * * * ? (매분 1회 실행)</li>
* </ul>
*
* <p>Job 파라미터 (JobDataMap):</p>
* <ul>
* <li>
* <b>api.status.error.range_minute</b>: API 장애기준 판단 시간간격 (단위: 분, 기본값: 1)
* <b>api.status.error.rate</b>: API 장애기준 오류건수 비율(%) (단위: 시간, 기본값: 100)
* <b>api.status.delay.range_minute</b>: API 지연기준 판단 시간간격 (단위: 분, 기본값: 1)
* <b>api.status.delay.avg_resp_time</b>: API 지연기준 평균응답시간 (단위: 밀리세컨드, 기본값: 10000)
* </li>
* </ul>
*
* </pre>
*/
@DisallowConcurrentExecution
public class ApiStatusMonitorJob implements Job {
private static final Logger log = LoggerFactory.getLogger(ApiStatusMonitorJob.class);
/** Job 파라미터 키: API 장애기준 시간간격(분) */
public static final String KEY_API_STATUS_ERROR_RANGE_MINUTE = "api.status.error.range_minute";
/** Job 파라미터 키: API 장애기준 오류건수 비율(%) */
public static final String KEY_API_STATUS_ERROR_RATE = "api.status.error.rate";
/** Job 파라미터 키: API 지연기준 시간간격(분) */
public static final String KEY_API_STATUS_DELAY_RANGE_MINUTE = "api.status.delay.range_minute";
/** Job 파라미터 키: API 지연기준 평균응답시간(ms) */
public static final String KEY_API_STATUS_DELAY_AVG_RESP_TIME = "api.status.delay.avg_resp_time";
public static final String DEFAULT_ERROR_RANGE_MINUTE = "1";
public static final String DEFAULT_ERROR_RATE = "100";
public static final String DEFAULT_DELAY_RANGE_MINUTE = "1";
public static final String DEFAULT_DELAY_AVG_RESP_TIME = "10000";
private transient MonitoringContext monitoringContext;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("*** START ApiStatusUpdateJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
// Job 파라미터 로깅
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
logJobParameters(jobDataMap);
HashMap<String, String> param = this.checkParameters(jobDataMap);
ApplicationContext appContext;
try {
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
} catch (SchedulerException e) {
log.error("applicationContext get module error", e);
return;
}
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
ApiStatusService apiStatusUpdateService = appContext.getBean(ApiStatusService.class);
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
apiStatusUpdateService.updateApiStatus(param);
} catch (Exception e) {
log.error("ApiStatusUpdateJob execution failed", e);
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
log.info("*** END ApiStatusUpdateJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
}
/**
* Job 파라미터 로깅
*/
private void logJobParameters(JobDataMap jobDataMap) {
if (jobDataMap == null || jobDataMap.isEmpty()) {
log.debug("Job 파라미터 없음 (기본값 사용)");
return;
}
StringBuilder sb = new StringBuilder("Job 파라미터: ");
for (String key : jobDataMap.getKeys()) {
sb.append(key).append("=").append(jobDataMap.getString(key)).append(", ");
}
log.info(sb.toString());
}
/**
* 집계 범위 시간 파라미터 파싱
*
* @param jobDataMap Job 파라미터 맵
* @return 집계 범위 시간 (파싱 실패 시 기본값 반환)
*/
private HashMap<String,String> checkParameters(JobDataMap jobDataMap) {
HashMap<String, String> param = new HashMap<String, String>();
if (jobDataMap == null) {
param.put("errorRangeMinute", DEFAULT_ERROR_RANGE_MINUTE); //1분동안
param.put("errorRate", DEFAULT_ERROR_RATE); //에러가 100% 발생시 '장애'로 판단
param.put("delayRangeMinute", DEFAULT_DELAY_RANGE_MINUTE); //1분동안
param.put("delayAvgRespTime", DEFAULT_DELAY_AVG_RESP_TIME); //평균 응답속도가 10초 이상이면 '지연'으로 판단
return param;
}
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_ERROR_RANGE_MINUTE))) {
param.put("errorRangeMinute", DEFAULT_ERROR_RANGE_MINUTE);
} else {
param.put("errorRangeMinute", jobDataMap.getString(KEY_API_STATUS_ERROR_RANGE_MINUTE));
}
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_ERROR_RATE))) {
param.put("errorRate", DEFAULT_ERROR_RATE);
} else {
param.put("errorRate", jobDataMap.getString(KEY_API_STATUS_ERROR_RATE));
}
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_DELAY_RANGE_MINUTE))) {
param.put("delayRangeMinute", DEFAULT_DELAY_RANGE_MINUTE);
} else {
param.put("delayRangeMinute", jobDataMap.getString(KEY_API_STATUS_DELAY_RANGE_MINUTE));
}
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_DELAY_AVG_RESP_TIME))) {
param.put("delayAvgRespTime", DEFAULT_DELAY_AVG_RESP_TIME);
} else {
param.put("delayAvgRespTime", jobDataMap.getString(KEY_API_STATUS_DELAY_AVG_RESP_TIME));
}
return param;
}
/**
* 개발 모드 여부 확인
* @return eai.systemmode=D 이면 true
*/
private boolean isDevMode() {
String systemMode = System.getProperty("eai.systemmode", "");
return "D".equalsIgnoreCase(systemMode);
}
}
@@ -0,0 +1,101 @@
package com.eactive.eai.rms.ext.djb.job;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.ext.djb.inflow.InflowTokenService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
/**
* Job - Quartz Job
* TSEAIFR11 데이터를 10분마다 조회하여 데이터가 있는 경우(유량제어 토큰 획득 실패), 알림을 발송한다
*
* <p>Cron Schedule 권장:</p>
* <ul>
* <li>기본: 0 0/10 * * * ? (매10분 1회 실행)</li>
* </ul>
*
* <p>Job 파라미터 (JobDataMap):</p>
* <ul>
* <li>
* NONE
* </li>
* </ul>
*
*/
@DisallowConcurrentExecution
public class InflowTokenMonitorJob implements Job {
private static final Logger log = LoggerFactory.getLogger(InflowTokenMonitorJob.class);
private transient MonitoringContext monitoringContext;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
long execMinute = 1; //default 배치 실행주기(분)
Trigger trigger = context.getTrigger();
Date prevFireTime = trigger.getPreviousFireTime();
Date nextFireTime = trigger.getNextFireTime();
//1회 이상 배치가 실행된 경우, 배치 간격을 구하여 유량제어조회 구간 파라미터로 전달한다
if (prevFireTime != null && nextFireTime != null) {
long diffMillis = nextFireTime.getTime() - prevFireTime.getTime();
execMinute = TimeUnit.MILLISECONDS.toMinutes(diffMillis);
log.info("실행 주기: {}분", execMinute);
}
ApplicationContext appContext;
try {
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
} catch (SchedulerException e) {
log.error("applicationContext get module error", e);
return;
}
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
InflowTokenService service = appContext.getBean(InflowTokenService.class);
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
service.checkRecentFails(execMinute);
} catch (Exception e) {
log.error("InflowTokenFailMonitorJob execution failed", e);
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
log.info("*** END InflowTokenFailMonitorJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
}
/**
* 개발 모드 여부 확인
* @return eai.systemmode=D 이면 true
*/
private boolean isDevMode() {
String systemMode = System.getProperty("eai.systemmode", "");
return "D".equalsIgnoreCase(systemMode);
}
}
@@ -0,0 +1,73 @@
package com.eactive.eai.rms.ext.djb.job;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.onl.apim.portalInquiry.PortalInquiryClosingService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 매일 00:05 실행 - RESPONDED 상태 문의글 자동 종료 배치
*
* <p>종료 조건 (기본 7일):</p>
* <ul>
* <li>답변완료 후 댓글이 7일간 없는 경우</li>
* <li>관리자 댓글 등록 후 7일간 포탈사용자 댓글이 없는 경우</li>
* </ul>
*
* <p>Cron: 0 5 0 * * ?</p>
*
* <p>Job 파라미터 (JobDataMap):</p>
* <ul>
* <li>{@value #KEY_INQUIRY_COMMENT_CLOSING_DAY}: 종료 기준 일수 (기본값: 7)</li>
* </ul>
*/
@DisallowConcurrentExecution
public class PortalInquiryClosingJob implements Job {
private static final Logger log = LoggerFactory.getLogger(PortalInquiryClosingJob.class);
/** Job 파라미터 키: 댓글종료 기간(일) */
public static final String KEY_INQUIRY_COMMENT_CLOSING_DAY = "inquiry.comment.closing_day";
private static final int DEFAULT_CLOSING_DAYS = 7;
@Autowired
private PortalInquiryClosingService inquiryCommentClosingService;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("*** START PortalInquiryCommentClosingService run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
int closingDays = DEFAULT_CLOSING_DAYS;
String closingDayParam = context.getMergedJobDataMap().getString(KEY_INQUIRY_COMMENT_CLOSING_DAY);
if (closingDayParam != null) {
try {
closingDays = Integer.parseInt(closingDayParam);
} catch (NumberFormatException e) {
log.warn("잘못된 {} 파라미터 값: '{}', 기본값 {}일 사용", KEY_INQUIRY_COMMENT_CLOSING_DAY, closingDayParam, DEFAULT_CLOSING_DAYS);
}
}
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
int closedCount = inquiryCommentClosingService.closeExpiredInquiries(closingDays);
log.info("PortalInquiryCommentClosingService 완료: {}건 CLOSED 처리", closedCount);
} catch (Exception e) {
log.error("PortalInquiryCommentClosingService execution failed", e);
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
log.info("*** END PortalInquiryCommentClosingService run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
}
}
@@ -0,0 +1,85 @@
package com.eactive.eai.rms.ext.djb.statistics;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequiredArgsConstructor
public class ApiUseStatsController {
private final ApiUseStatsService service;
private final ApiUseStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiUseStatsMan.view")
public String view() {
return "/onl/kjb/statistics/apiUseStatsMan";
}
@PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) {
Page<ApiStatsUI> page = service.selectList(search, pageable);
return ResponseEntity.ok(new GridResponse<>(page));
}
@PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search);
try {
List<ApiStatsUI> uiList = service.selectList(search);
log.info("Data retrieved - count: {}", uiList.size());
if (uiList.isEmpty()) {
log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
response.getWriter().flush();
return;
}
String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName);
excelExportService.exportApiStats(uiList, fileName, response);
log.info("Excel export completed successfully");
} catch (Exception e) {
log.error("Excel export failed - error: {}", e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
response.getWriter().flush();
} else {
log.error("Cannot send error response - response already committed");
}
}
}
private String generateFileName(ApiStatsSearch search) {
return "API사용현황_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
}
}
@@ -0,0 +1,256 @@
package com.eactive.eai.rms.ext.djb.statistics;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
import lombok.extern.slf4j.Slf4j;
/**
* API 통계 Excel Export 서비스
* Hour/Minute 통계 데이터를 Excel 파일로 생성
*/
@Slf4j
@Service
public class ApiUseStatsExcelExportService {
private static final String[] COLUMN_HEADERS = {
"구분", "총건수", "성공", "성공율(%)", "실패율(%)", "Timeout", "시스템오류",
"평균응답(ms)", "최소응답(ms)", "최대응답(ms)"
};
/**
* API 통계 데이터를 Excel 파일로 생성하여 HTTP 응답으로 전송
*
* @param dataList API 통계 데이터 리스트
* @param fileName 다운로드 파일명
* @param response HTTP 응답 객체
* @throws IOException 파일 생성 실패 시
*/
public void exportApiStats(List<ApiStatsUI> dataList, String fileName, HttpServletResponse response)
throws IOException {
log.info("Starting Excel export - rows: {}, filename: {}", dataList.size(), fileName);
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("API통계");
log.debug("Excel sheet created");
// 스타일 생성
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle stringStyle = createStringStyle(workbook);
CellStyle numberStyle = createNumberStyle(workbook);
CellStyle decimalStyle = createDecimalStyle(workbook);
log.debug("Excel styles created");
// 헤더 행 생성
createHeaderRow(sheet, headerStyle);
log.debug("Header row created");
// 데이터 행 생성
int rowNum = 1;
for (ApiStatsUI data : dataList) {
createDataRow(sheet, rowNum++, data, stringStyle, numberStyle, decimalStyle);
}
log.info("Data rows created - count: {}", dataList.size());
// 컬럼 너비 자동 조정
autoSizeColumns(sheet);
log.debug("Column widths adjusted");
// HTTP 응답 설정
setHttpResponse(response, fileName, workbook);
log.info("Excel file sent to response");
} catch (Exception e) {
log.error("Error creating Excel file", e);
throw new IOException("Excel file creation failed: " + e.getMessage(), e);
}
}
/**
* 헤더 스타일 생성 (회색 배경, 볼드, 테두리)
*/
private CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
// 배경색
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 테두리
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
// 정렬
style.setAlignment(HorizontalAlignment.CENTER);
// 폰트 (볼드)
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}
/**
* 문자열 데이터 스타일 생성 (기본 테두리)
*/
private CellStyle createStringStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
return style;
}
/**
* 숫자 데이터 스타일 생성 (우측정렬 + 쉼표 구분)
*/
private CellStyle createNumberStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("#,##0"));
return style;
}
/**
* 소수점 데이터 스타일 생성 (우측정렬 + 소수점 2자리)
*/
private CellStyle createDecimalStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("0.00"));
return style;
}
/**
* 헤더 행 생성
*/
private void createHeaderRow(Sheet sheet, CellStyle headerStyle) {
Row headerRow = sheet.createRow(0);
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(COLUMN_HEADERS[i]);
cell.setCellStyle(headerStyle);
}
}
/**
* 데이터 행 생성
*/
private void createDataRow(Sheet sheet, int rowNum, ApiStatsUI data,
CellStyle stringStyle, CellStyle numberStyle, CellStyle decimalStyle) {
Row row = sheet.createRow(rowNum);
int colNum = 0;
createStringCell(row, colNum++, data.getOrgName(), stringStyle);
createLongCell(row, colNum++, data.getTotalCnt(), numberStyle);
createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle);
createDecimalCell(row, colNum++, data.getSuccessRate(), decimalStyle);
createDecimalCell(row, colNum++, data.getFailRate(), decimalStyle);
createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle);
createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle);
createDecimalCell(row, colNum++, data.getAvgRespTime(), numberStyle);
createDecimalCell(row, colNum++, data.getMinRespTime(), numberStyle);
createDecimalCell(row, colNum++, data.getMaxRespTime(), numberStyle);
}
/**
* 문자열 셀 생성
*/
private void createStringCell(Row row, int colNum, String value, CellStyle style) {
Cell cell = row.createCell(colNum);
cell.setCellValue(value != null ? value : "");
cell.setCellStyle(style);
}
/**
* Long 타입 숫자 셀 생성
*/
private void createLongCell(Row row, int colNum, Long value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0);
}
cell.setCellStyle(style);
}
/**
* BigDecimal 타입 소수점 셀 생성
*/
private void createDecimalCell(Row row, int colNum, BigDecimal value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0.0);
}
cell.setCellStyle(style);
}
/**
* 컬럼 너비 자동 조정
*/
private void autoSizeColumns(Sheet sheet) {
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
sheet.autoSizeColumn(i);
// 한글 문자 고려하여 약간 여유 공간 추가
sheet.setColumnWidth(i, sheet.getColumnWidth(i) + 512);
}
}
/**
* HTTP 응답 설정 및 파일 전송
*/
private void setHttpResponse(HttpServletResponse response, String fileName, Workbook workbook)
throws IOException {
// Content-Type 설정
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// Content-Disposition 설정 (파일명 UTF-8 인코딩)
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString())
.replaceAll("\\+", "%20");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
// Workbook을 응답 스트림으로 전송
workbook.write(response.getOutputStream());
response.getOutputStream().flush();
}
}
@@ -0,0 +1,12 @@
package com.eactive.eai.rms.ext.djb.statistics;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId;
/**
* API 사용현황 Repository
*/
interface ApiUseStatsRepository extends BaseRepository<ApiStatsDay, ApiStatsDayId> {
}
@@ -0,0 +1,209 @@
package com.eactive.eai.rms.ext.djb.statistics;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
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.common.util.StringUtils;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@Service
@Transactional
public class ApiUseStatsService
extends AbstractDataService<ApiStatsDay, ApiStatsDayId, ApiUseStatsRepository> {
private static final int MAX_DAYS = 31;
@PersistenceContext
private EntityManager entityManager;
@SuppressWarnings("unchecked")
public Page<ApiStatsUI> selectList(ApiStatsSearch search, Pageable pageable) {
Query dataQuery = getDataQuery(search);
dataQuery.setFirstResult((int) pageable.getOffset());
dataQuery.setMaxResults(pageable.getPageSize());
List<Object[]> rows = dataQuery.getResultList();
List<ApiStatsUI> results = rows.stream()
.map(this::toVO)
.collect(Collectors.toList());
long records = rows.size() > 0 ? StringUtils.toLong(rows.get(0)[0]) : 0;
return new PageImpl<>(results, pageable, records);
}
@SuppressWarnings("unchecked")
public List<ApiStatsUI> selectList(ApiStatsSearch search) {
Query dataQuery = getDataQuery(search);
List<Object[]> rows = dataQuery.getResultList();
return rows.stream()
.map(this::toVO)
.collect(Collectors.toList());
}
private Query getDataQuery(ApiStatsSearch search) {
StringBuilder where = buildNativeWhere(search);
String dataSql = "";
if ("ORG".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", B.ORGNAME"
+ ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", SUM(A.TIMEOUT_CNT)"
+ ", SUM(A.SYSTEM_ERR_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
+ ", MIN(A.MIN_RESP_TIME)"
+ ", MAX(A.MAX_RESP_TIME)"
+ " FROM API_STATS_DAY A"
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
+ where
+ " GROUP BY B.ORGNAME"
+ " ORDER BY ORGNAME";
} else if ("API".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", C.EAISVCDESC"
+ ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", SUM(A.TIMEOUT_CNT)"
+ ", SUM(A.SYSTEM_ERR_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
+ ", MIN(A.MIN_RESP_TIME)"
+ ", MAX(A.MAX_RESP_TIME)"
+ " FROM API_STATS_DAY A "
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
+ where
+ " GROUP BY C.EAISVCDESC"
+ " ORDER BY EAISVCDESC";
} else if ("DATE".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME "
+ ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
+ ", SUM(A.TIMEOUT_CNT)"
+ ", SUM(A.SYSTEM_ERR_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
+ ", MIN(A.MIN_RESP_TIME)"
+ ", MAX(A.MAX_RESP_TIME)"
+ " FROM API_STATS_DAY A "
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
+ where
+ " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')"
+ " ORDER BY STAT_TIME";
}
Query dataQuery = entityManager.createNativeQuery(dataSql);
if (search.getParsedStartDate() != null) {
dataQuery.setParameter("searchStartDate", search.getParsedStartDate());
dataQuery.setParameter("searchEndDate", search.getParsedEndDate());
}
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
dataQuery.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%");
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
dataQuery.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%");
}
return dataQuery;
}
/**
* Native Query 결과를 ApiStatsUI로 변환
*/
private ApiStatsUI toVO(Object[] row) {
ApiStatsUI vo = new ApiStatsUI();
vo.setOrgName(StringUtils.toString(row[1]));
vo.setTotalCnt(StringUtils.toLong(row[2]));
vo.setSuccessCnt(StringUtils.toLong(row[3]));
vo.setSuccessRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP));
vo.setFailRate(StringUtils.toDecimal(row[5]).setScale(2, RoundingMode.HALF_UP));
vo.setTimeoutCnt(StringUtils.toLong(row[6]));
vo.setSystemErrCnt(StringUtils.toLong(row[7]));
vo.setAvgRespTime(StringUtils.toDecimal(row[8]));
vo.setMinRespTime(StringUtils.toDecimal(row[9]));
vo.setMaxRespTime(StringUtils.toDecimal(row[10]));
return vo;
}
/**
* native SQL용 동적 WHERE 절 생성
*/
private StringBuilder buildNativeWhere(ApiStatsSearch search) {
StringBuilder where = new StringBuilder(" WHERE 1=1");
calculateDateRange(search);
if (search.getParsedStartDate() != null) {
where.append(" AND A.STAT_TIME >= :searchStartDate ");
where.append(" AND A.STAT_TIME <= :searchEndDate ");
}
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
where.append(" AND UPPER(B.ORGNAME) LIKE :searchOrgName ");
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
where.append(" AND UPPER(C.EAISVCDESC) LIKE :searchApiName ");
}
return where;
}
/**
* 조회 기간 계산 (최대 31일 제한)
*/
private void calculateDateRange(ApiStatsSearch search) {
if (StringUtils.isBlank(search.getSearchStartDateTime())) {
return;
}
LocalDate startDate = LocalDate.parse(search.getSearchStartDateTime(),
DateTimeFormatter.ofPattern("yyyyMMdd"));
LocalDate endDate;
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
endDate = LocalDate.parse(search.getSearchEndDateTime(),
DateTimeFormatter.ofPattern("yyyyMMdd"));
if (java.time.Period.between(startDate, endDate).getDays() > MAX_DAYS) {
endDate = startDate.plusDays(MAX_DAYS);
}
} else {
endDate = startDate.plusDays(MAX_DAYS);
}
search.setParsedStartDate(startDate);
search.setParsedEndDate(endDate);
}
}
@@ -0,0 +1,89 @@
package com.eactive.eai.rms.ext.djb.util;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class UmsManager {
private static final Logger log = LoggerFactory.getLogger(UmsManager.class);
private final MessageHandlerService messageHandlerService;
private final UserInfoService userInfoService;
private final WebhookService webhookService;
/**
* role 역할을 가진 내부직원에게 메신저(Swing chat) 발송
* @param role
* @param messageCode
* @param message
* @return
*/
public void sendMessenger(String roleId, MessageCode messageCode, String message) {
List<UserInfo> users = userInfoService.findAllByRoleId(roleId);
if (users.isEmpty()) {
log.info("sendMessenger: no users found for role '{}'", roleId);
return;
}
for (UserInfo user : users) {
MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid())
.username(user.getUsername())
.build();
HashMap<String, Object> params = new HashMap<>();
params.put("message", message);
messageHandlerService.publishEvent(messageCode, recipient, params);
}
}
/**
* 내부직원에게 메신저 발송
* @param user
* @param messageCode
* @param message
*/
public void sendMessenger(UserInfo user, MessageCode messageCode, String message) {
MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid())
.username(user.getUsername())
.build();
HashMap<String, Object> params = new HashMap<>();
params.put("message", message);
messageHandlerService.publishEvent(messageCode, recipient, params);
}
/**
* event를 구독중인 제휴사에게 웹훅 발송 (비동기)
* @param event
* @param apiIds
*/
@Async("webhookExecutor")
public void sendWebhook(String event, List<String> apiIds) {
List<WebhookSendRequest> list = webhookService.findSendList(event, apiIds);
for (WebhookSendRequest req : list) {
webhookService.send(req);
}
}
}
@@ -0,0 +1,26 @@
package com.eactive.eai.rms.ext.djb.webhook.config;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class WebhookAsyncConfig {
@Bean(name = "webhookExecutor")
public Executor webhookExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("webhook-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
@@ -0,0 +1,127 @@
package com.eactive.eai.rms.ext.djb.webhook.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookReceiveService;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
@RequestMapping("/api/webhook")
@RequiredArgsConstructor
public class WebhookSendController {
private final WebhookService webhookService;
private final WebhookReceiveService webhookReceiveService;
private static final int KEY_BYTE_LENGTH = 32; // 256bit
/* ------------------------------------------------------------------ */
/* 발송 */
/* ------------------------------------------------------------------ */
@PostMapping("/send.json")
public ResponseEntity<Map<String, Object>> sendWebhook(
@RequestBody WebhookSendRequest request) {
webhookService.send(request);
Map<String, Object> response = new HashMap<>();
// response.put("logId", result.getId());
// response.put("success", result.getSuccess());
// response.put("statusCode", result.getStatusCode());
return ResponseEntity.ok(response);
}
/* ------------------------------------------------------------------ */
/* 수신 */
/* ------------------------------------------------------------------ */
/**
* 웹훅 수신 엔드포인트
*
* 헤더 예시:
* X-Webhook-Signature : sha256={hmac값}
* X-Webhook-Event : ORDER_CREATED
* X-Webhook-Timestamp : 1712345678901
*/
@PostMapping("/receive")
public ResponseEntity<Map<String, Object>> receiveWebhook(
@RequestHeader(value = "X-Webhook-Signature", required = false) String signature,
@RequestHeader(value = "X-Webhook-Event", required = false) String eventType,
@RequestHeader(value = "X-Webhook-Timestamp", required = false) String timestamp,
@RequestBody String rawPayload) {
log.info("[Webhook] 수신 - eventType: {}, timestamp: {}", eventType, timestamp);
// 1. 필수 헤더 누락 체크
if (signature == null || eventType == null || timestamp == null) {
log.warn("[Webhook] 수신 거부 - 필수 헤더 누락");
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse("필수 헤더가 누락되었습니다."));
}
// 2. 서명 검증
boolean isValid = webhookReceiveService.verifySignature(rawPayload, signature);
if (!isValid) {
log.warn("[Webhook] 수신 거부 - 서명 불일치 / eventType: {}", eventType);
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(errorResponse("서명 검증에 실패하였습니다."));
}
// 3. 타임스탬프 유효성 검증 (5분 이내 요청만 허용)
boolean isTimestampValid = webhookReceiveService.verifyTimestamp(timestamp);
if (!isTimestampValid) {
log.warn("[Webhook] 수신 거부 - 타임스탬프 만료 / eventType: {}", eventType);
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(errorResponse("요청이 만료되었습니다."));
}
// 4. 이벤트 처리
try {
webhookReceiveService.process(eventType, rawPayload);
} catch (Exception e) {
log.error("[Webhook] 이벤트 처리 실패 - eventType: {}, error: {}", eventType, e.getMessage(), e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse("이벤트 처리 중 오류가 발생하였습니다."));
}
// 5. 정상 응답
Map<String, Object> response = new HashMap<>();
response.put("result", "OK");
response.put("eventType", eventType);
return ResponseEntity.ok(response);
}
/* ------------------------------------------------------------------ */
/* 공통 */
/* ------------------------------------------------------------------ */
private Map<String, Object> errorResponse(String message) {
Map<String, Object> error = new HashMap<>();
error.put("result", "ERROR");
error.put("message", message);
return error;
}
}
@@ -0,0 +1,31 @@
package com.eactive.eai.rms.ext.djb.webhook.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WebhookPayload {
private String eventType;
private String eventId;
private long timestamp;
private Object data;
public static WebhookPayload of(String eventType, Object data) {
return WebhookPayload.builder()
.eventType(eventType)
.eventId(UUID.randomUUID().toString())
.timestamp(System.currentTimeMillis())
.data(data)
.build();
}
}
@@ -0,0 +1,14 @@
package com.eactive.eai.rms.ext.djb.webhook.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class WebhookSendRequest {
private String orgId;
private String targetUrl;
private String eventType;
private String secret;
private Object data;
}
@@ -0,0 +1,11 @@
package com.eactive.eai.rms.ext.djb.webhook.repository;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookReq;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
@EMSDataSource
public interface WebhookReqRepository extends JpaRepository<WebhookReq, String> {
}
@@ -0,0 +1,31 @@
package com.eactive.eai.rms.ext.djb.webhook.repository;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
@Repository
@EMSDataSource
public interface WebhookSendLogRepository extends JpaRepository<WebhookSendLog, Long> {
// 오라클 CHAR(1) Y/N 기준 조회
@Query("SELECT l FROM WebhookSendLog l " +
"WHERE l.success = 'N' AND l.retryCount < :maxRetry")
List<WebhookSendLog> findFailedLogs(@Param("maxRetry") int maxRetry);
@Query("SELECT l FROM WebhookSendLog l " +
"WHERE l.eventType = :eventType " +
"AND l.createdAt BETWEEN :from AND :to")
List<WebhookSendLog> findByEventTypeAndPeriod(
@Param("eventType") String eventType,
@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to);
}

Some files were not shown because too many files have changed in this diff Show More