Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a92fc85730 | |||
| 317634b5b7 | |||
| 27fa79ddd9 | |||
| ccc55fd015 | |||
| 3bf8370d0c | |||
| 46bd79bc36 | |||
| cdc1a6f342 | |||
| 42d7fa977e | |||
| cc0ef13d5d | |||
| 6604f7c9e8 | |||
| cd8f826ba5 | |||
| 031796dac4 | |||
| f091b9ab19 | |||
| f47480674d | |||
| cd20aeee2a | |||
| 0365f4342e | |||
| 0576fd4c6e | |||
| 8215b596fe | |||
| fc6b96d7cd | |||
| 5fe247df65 | |||
| d11f7d9f38 | |||
| 889fd1f9f5 | |||
| c0844e8d36 | |||
| db38b8f6e9 | |||
| 8abe0a8585 | |||
| ca8d46c7b8 | |||
| 628ab01c13 | |||
| f10f5ce08e | |||
| 90ba584e4b | |||
| e59f64653f | |||
| 2422c7f01e | |||
| 29025d5f47 | |||
| e91b12f2d7 | |||
| da90a3a002 | |||
| a4614ca7fd | |||
| 2d414dc31b | |||
| 775d69eb53 | |||
| 8f4162569d | |||
| adc5371df8 | |||
| e7297458f6 | |||
| c10bfe8326 | |||
| c224f80cdf | |||
| 6c70d9611b | |||
| 26679cf649 | |||
| 406dd34e28 | |||
| 59dd28c23b | |||
| cd93e34840 | |||
| 2fddffe29d | |||
| 7a73ef370f | |||
| c9be6069ae | |||
| a13c8f154f | |||
| 8c98297634 | |||
| f36bfcc642 | |||
| 2833944fe4 | |||
| 18110641d6 | |||
| 316f787276 | |||
| c3c9f7a2da | |||
| 95e31befbb | |||
| f1f7576b9d | |||
| 545e09891b | |||
| 70c2ac8376 | |||
| 21c493cb0e | |||
| b9c6a0da06 | |||
| a0725b57a6 | |||
| 50bfcacf27 | |||
| f400f0322f | |||
| d0cd8118f3 | |||
| 07e65c055b | |||
| af8a9cca51 | |||
| 39a09d0f5d | |||
| 5b940717c4 | |||
| 8ccf52fd66 | |||
| dedc58a07f | |||
| 4530af9dec | |||
| cbf3066a56 | |||
| 160e28444a | |||
| d7dcf9a81c | |||
| acfe6b2d03 |
Vendored
+15
@@ -90,6 +90,21 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SBOM(CycloneDX) -> xlsx. 산출물 eapim-admin-sbom.xlsx 를 아티팩트로 보관.
|
||||||
|
// 실패해도 배포는 진행하도록 UNSTABLE 로만 표시한다.
|
||||||
|
stage('SBOM') {
|
||||||
|
steps {
|
||||||
|
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
||||||
|
sh 'gradle sbomXlsx --no-daemon -Pprofile=weblogic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/sbom/*.xlsx', fingerprint: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,5 @@ PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK,DELETE
|
|||||||
PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE
|
PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE
|
||||||
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
|
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
|
||||||
MessageRequestManController_메세지발송내역_APIGW_UPDATE_STATUS
|
MessageRequestManController_메세지발송내역_APIGW_UPDATE_STATUS
|
||||||
|
PortalInquiryManController_Q&A문의관리_APIGW_VISIBILITY
|
||||||
|
PortalMenuManController_포탈메뉴관리_APIGW_INSERT,UPDATE,DELETE,TRANSACTION_PLACEMENT,INITIALIZE,TRANSACTION_RELOAD
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
||||||
targetNamespace="http://xmlns.oracle.com/weblogic/weblogic-web-app"
|
|
||||||
xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app"
|
|
||||||
elementFormDefault="qualified">
|
|
||||||
|
|
||||||
<xs:element name="weblogic-web-app">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:all>
|
|
||||||
<xs:element name="context-root" type="xs:string" minOccurs="0"/>
|
|
||||||
<xs:element name="session-descriptor" minOccurs="0">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:all>
|
|
||||||
<xs:element name="timeout-secs" type="xs:integer" minOccurs="0"/>
|
|
||||||
<xs:element name="persistent-store-type" type="xs:string" minOccurs="0"/>
|
|
||||||
</xs:all>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="container-descriptor" minOccurs="0">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:all>
|
|
||||||
<xs:element name="prefer-application-packages" minOccurs="0">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element name="package-name" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="prefer-application-resources" minOccurs="0">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:sequence>
|
|
||||||
<xs:element name="resource-name" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
|
|
||||||
</xs:sequence>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
</xs:all>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
</xs:all>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
|
|
||||||
</xs:schema>
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<weblogic-web-app
|
<weblogic-web-app xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app">
|
||||||
xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://xmlns.oracle.com/weblogic/weblogic-web-app weblogic-web-app.xsd">
|
|
||||||
|
|
||||||
<context-root>monitoring</context-root>
|
<context-root>monitoring</context-root>
|
||||||
<session-descriptor>
|
<session-descriptor>
|
||||||
<timeout-secs>1800</timeout-secs>
|
<timeout-secs>1800</timeout-secs>
|
||||||
|
<cookie-name>JSESSIONID_EMS</cookie-name>
|
||||||
<persistent-store-type>replicated_if_clustered</persistent-store-type>
|
<persistent-store-type>replicated_if_clustered</persistent-store-type>
|
||||||
</session-descriptor>
|
</session-descriptor>
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ header.sub{position:relative; padding-top:80px;}
|
|||||||
.gnb .depth1 > li:hover > a{opacity:1;}
|
.gnb .depth1 > li:hover > a{opacity:1;}
|
||||||
.gnb .depth1 > li > .red_box{display:block; position:absolute; width:100%; height:2px; top:78px; box-sizing:border-box; background:#ed1c24; transform-origin:center center; transform:scaleX(0); transition:all 0.3s;}
|
.gnb .depth1 > li > .red_box{display:block; position:absolute; width:100%; height:2px; top:78px; box-sizing:border-box; background:#ed1c24; transform-origin:center center; transform:scaleX(0); transition:all 0.3s;}
|
||||||
*/
|
*/
|
||||||
.gnb .depth2{position:absolute; left:0; top:80px; width:160px; height:310px; box-sizing:border-box; padding:20px 0; border-right:1px solid #eee; text-align:center; z-index:9;}
|
.gnb .depth2{position:absolute; left:0; top:80px; width:160px; height:280px; box-sizing:border-box; padding:20px 0; border-right:1px solid #eee; text-align:center; z-index:9;}
|
||||||
.gnb .depth1 > li:first-child .depth2{border-left:1px solid #eee;}
|
.gnb .depth1 > li:first-child .depth2{border-left:1px solid #eee;}
|
||||||
.gnb .depth2 > li > a{display:block; color:#000; font-size:13px; line-height:30px;}
|
.gnb .depth2 > li > a{display:block; color:#000; font-size:13px; line-height:30px;}
|
||||||
.gnb .depth2 > li > a:hover{color:#ff0000;}
|
.gnb .depth2 > li > a:hover{color:#ff0000;}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,334 @@
|
|||||||
|
/**
|
||||||
|
* Editor Content Styles (editor-content.css)
|
||||||
|
*
|
||||||
|
* Summernote 에디터로 작성된 HTML 콘텐츠의 공통 스타일
|
||||||
|
* 관리자포탈/개발자포탈 양쪽에서 동일하게 사용
|
||||||
|
*
|
||||||
|
* 사용법:
|
||||||
|
* - 관리자포탈: Summernote .note-editable에 .editor-content 클래스 추가
|
||||||
|
* - 개발자포탈: 콘텐츠 wrapper에 .editor-content 클래스 추가
|
||||||
|
*
|
||||||
|
* @version 1.0.0
|
||||||
|
* @date 2025-12-30
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
CSS Reset + 기본 설정
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
all: revert;
|
||||||
|
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
line-height: 1.8 !important;
|
||||||
|
color: #1A1A2E !important;
|
||||||
|
word-wrap: break-word;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
헤딩 (Headings)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content h1 {
|
||||||
|
font-size: 20px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
margin: 32px 0 16px !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
color: #1A1A2E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content h1:first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content h2 {
|
||||||
|
font-size: 18px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
margin: 24px 0 8px !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
color: #1A1A2E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content h3 {
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
margin: 16px 0 8px !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
color: #1A1A2E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content h4,
|
||||||
|
.editor-content h5,
|
||||||
|
.editor-content h6 {
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
margin: 16px 0 8px !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
color: #1A1A2E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
단락 (Paragraphs)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content p {
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
line-height: 1.8 !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content p:last-child {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
리스트 (Lists) - 글로벌 reset 대응
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content ul {
|
||||||
|
list-style-type: disc !important;
|
||||||
|
padding-left: 32px !important;
|
||||||
|
margin: 16px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ol {
|
||||||
|
list-style-type: decimal !important;
|
||||||
|
padding-left: 32px !important;
|
||||||
|
margin: 16px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content li {
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
line-height: 1.8 !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content li:last-child {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 중첩 리스트 */
|
||||||
|
.editor-content ul ul {
|
||||||
|
list-style-type: circle !important;
|
||||||
|
margin: 8px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ul ul ul {
|
||||||
|
list-style-type: square !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ol ol {
|
||||||
|
list-style-type: lower-alpha !important;
|
||||||
|
margin: 8px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ol ol ol {
|
||||||
|
list-style-type: lower-roman !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
테이블 (Tables)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content table {
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse !important;
|
||||||
|
margin: 24px 0 !important;
|
||||||
|
border: 1px solid #E2E8F0 !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content th,
|
||||||
|
.editor-content td {
|
||||||
|
border: 1px solid #E2E8F0 !important;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
text-align: left !important;
|
||||||
|
vertical-align: top !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content th {
|
||||||
|
background: #EFF6FF !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #1A1A2E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content tr:hover {
|
||||||
|
background: #F8FAFC !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
링크 (Links)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content a {
|
||||||
|
color: #0049b4 !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
transition: color 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content a:hover {
|
||||||
|
color: #003080 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
인용문 (Blockquote)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content blockquote {
|
||||||
|
border-left: 4px solid #0049b4 !important;
|
||||||
|
padding: 16px 24px !important;
|
||||||
|
margin: 24px 0 !important;
|
||||||
|
background: #F8FAFC !important;
|
||||||
|
font-style: italic !important;
|
||||||
|
color: #64748B !important;
|
||||||
|
border-radius: 0 8px 8px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content blockquote p {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
이미지 (Images)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
margin: 16px 0 !important;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
코드 (Code)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content code {
|
||||||
|
background: #F8FAFC !important;
|
||||||
|
padding: 2px 6px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
font-family: 'Fira Code', 'Courier New', monospace !important;
|
||||||
|
font-size: 0.9em !important;
|
||||||
|
color: #0049b4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content pre {
|
||||||
|
background: #F8FAFC !important;
|
||||||
|
border: 1px solid #E2E8F0 !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
margin: 16px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content pre code {
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
color: #1A1A2E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
구분선 (Horizontal Rule)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content hr {
|
||||||
|
border: none !important;
|
||||||
|
border-top: 1px solid #E2E8F0 !important;
|
||||||
|
margin: 32px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
텍스트 강조 (Text Emphasis)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.editor-content strong,
|
||||||
|
.editor-content b {
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content em,
|
||||||
|
.editor-content i {
|
||||||
|
font-style: italic !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content u {
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content s,
|
||||||
|
.editor-content strike,
|
||||||
|
.editor-content del {
|
||||||
|
text-decoration: line-through !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content mark {
|
||||||
|
background-color: #FEF3C7 !important;
|
||||||
|
padding: 0 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content sub {
|
||||||
|
vertical-align: sub !important;
|
||||||
|
font-size: 0.8em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content sup {
|
||||||
|
vertical-align: super !important;
|
||||||
|
font-size: 0.8em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
반응형 (Responsive)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.editor-content {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content h1 {
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content h2 {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content h3,
|
||||||
|
.editor-content h4,
|
||||||
|
.editor-content h5,
|
||||||
|
.editor-content h6 {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content p,
|
||||||
|
.editor-content li {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content table {
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content th,
|
||||||
|
.editor-content td {
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ul,
|
||||||
|
.editor-content ol {
|
||||||
|
padding-left: 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content blockquote {
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content pre {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// OpenAPI 에디터 초기 상태 골격(빈 껍데기).
|
||||||
|
// - specToData() 가 deepClone 하여 서버 spec 값으로 덮어쓰는 구조 기준값.
|
||||||
|
// - 실제 데모/샘플 값은 두지 않는다(팝업은 항상 서버 spec 로드 후 렌더).
|
||||||
|
// - docOptions 만 기능 기본값으로 유지(문서 미리보기 옵션).
|
||||||
|
// locked:true = 게이트웨이 자동 주입(회색+자물쇠) / locked:false = 사용자 편집.
|
||||||
|
|
||||||
|
window.SAMPLE_DATA = (function () {
|
||||||
|
function f() { return { value: '', locked: false }; }
|
||||||
|
return {
|
||||||
|
info: {
|
||||||
|
title: f(),
|
||||||
|
version: f(),
|
||||||
|
summary: f(),
|
||||||
|
description: f(),
|
||||||
|
termsOfService: f(),
|
||||||
|
contact: { name: f(), email: f(), url: f() },
|
||||||
|
license: { name: f(), url: f() }
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: [],
|
||||||
|
|
||||||
|
externalDocs: { description: f(), url: f() },
|
||||||
|
|
||||||
|
servers: [],
|
||||||
|
|
||||||
|
serverVariables: [],
|
||||||
|
|
||||||
|
operation: {
|
||||||
|
method: f(),
|
||||||
|
path: f(),
|
||||||
|
operationId: f(),
|
||||||
|
summary: f(),
|
||||||
|
description: f(),
|
||||||
|
tags: { value: [], locked: false },
|
||||||
|
deprecated: { value: false, locked: false }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 파라미터 (Path/Query/Header/Cookie)
|
||||||
|
parameters: [],
|
||||||
|
|
||||||
|
// Request Body 스키마
|
||||||
|
requestBody: {
|
||||||
|
mediaType: 'application/json',
|
||||||
|
required: { value: false, locked: false },
|
||||||
|
schema: []
|
||||||
|
},
|
||||||
|
|
||||||
|
// 응답 스키마 (상태코드 별)
|
||||||
|
responses: {
|
||||||
|
'200': { description: f(), schema: [] }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 보안 스킴
|
||||||
|
securitySchemes: [],
|
||||||
|
globalSecurity: [],
|
||||||
|
|
||||||
|
// 예제
|
||||||
|
examples: {
|
||||||
|
request: {},
|
||||||
|
response: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 문서 옵션
|
||||||
|
docOptions: {
|
||||||
|
theme: 'light',
|
||||||
|
lang: 'ko',
|
||||||
|
includeExamples: true,
|
||||||
|
tryItOut: true,
|
||||||
|
defaultExpandDepth: -1,
|
||||||
|
layout: 'BaseLayout',
|
||||||
|
tagFilter: [],
|
||||||
|
sideToc: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/* DJB OpenAPI Editor POC — Tailwind 보강 커스텀 */
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo",
|
||||||
|
"Malgun Gothic", "Noto Sans KR", system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 잠금 행/카드 좌측 띠 === */
|
||||||
|
.locked-row td:first-child {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.locked-row td:first-child::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 6px;
|
||||||
|
bottom: 6px;
|
||||||
|
width: 3px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.locked-card {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(to right, #f8fafc 0%, #ffffff 4px) #ffffff;
|
||||||
|
}
|
||||||
|
.locked-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 8px; bottom: 8px;
|
||||||
|
width: 3px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 스키마 테이블 — 가독성 보조 === */
|
||||||
|
.schema-table th { white-space: nowrap; }
|
||||||
|
.schema-table tbody tr:hover { background: #fafbfc; }
|
||||||
|
.schema-table input,
|
||||||
|
.schema-table select {
|
||||||
|
font-size: 12.5px;
|
||||||
|
}
|
||||||
|
.schema-table input[type="text"] {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 스텝퍼 항목 hover === */
|
||||||
|
.step-item:hover { background: #f8fafc; }
|
||||||
|
|
||||||
|
/* === 미리보기 패널: Swagger UI 크기 미세조정 === */
|
||||||
|
#preview-swagger .swagger-ui {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
#preview-swagger .swagger-ui .info { margin: 18px 0; }
|
||||||
|
#preview-swagger .swagger-ui .info .title { font-size: 22px; }
|
||||||
|
#preview-swagger .swagger-ui .scheme-container { padding: 12px 0; box-shadow: none; }
|
||||||
|
#preview-swagger .swagger-ui .opblock { margin: 0 0 12px 0; }
|
||||||
|
|
||||||
|
/* === 다크 테마 (POC 시뮬레이션 — Swagger 자체 다크는 미지원이라 컨테이너만) === */
|
||||||
|
body.theme-dark #preview-swagger { background: #1e293b; }
|
||||||
|
|
||||||
|
/* === 토스트 애니메이션 === */
|
||||||
|
#toast { transition: opacity 0.2s; }
|
||||||
|
|
||||||
|
/* === Monaco 컨테이너가 hidden 일 때 layout 깨짐 방지 === */
|
||||||
|
#preview-monaco {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Stepper 6단 grid 셀 좌우 라인 (마지막 셀 제외) === */
|
||||||
|
#stepper li {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#stepper li:not(:last-child)::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: -8px;
|
||||||
|
width: 16px;
|
||||||
|
height: 1px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 보안 스킴 카드 — 잠금 표시 === */
|
||||||
|
.locked-card {
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 칩(태그) 미세 조정 === */
|
||||||
|
.tag-chip { transition: background 0.15s; }
|
||||||
|
|
||||||
|
/* === Export menu fade === */
|
||||||
|
#export-menu { animation: fadeIn 0.12s ease-out; }
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === 1600px 기준 폼 패널 내부 가로 여유 확보 === */
|
||||||
|
#form-content { max-width: 100%; }
|
||||||
|
|
||||||
|
/* === readonly input 위에서 텍스트 커서 안 보이게 === */
|
||||||
|
input[readonly] { user-select: text; }
|
||||||
@@ -184,8 +184,9 @@ function getEndTime(){
|
|||||||
</td>
|
</td>
|
||||||
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.remoteAddress") %></th>
|
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.remoteAddress") %></th>
|
||||||
<td><input type="text" name="searchRemoteAddress" value="${param.searchRemoteAddress}" style="width:100%"></td>
|
<td><input type="text" name="searchRemoteAddress" value="${param.searchRemoteAddress}" style="width:100%"></td>
|
||||||
<th style="width:180px;"></th>
|
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.parameters") %></th>
|
||||||
<td>
|
<td>
|
||||||
|
<input type="text" name="searchParameters" value="${param.searchParameters}" style="width:100%">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -18,6 +18,16 @@
|
|||||||
<script language="javascript" >
|
<script language="javascript" >
|
||||||
var url = '<c:url value="/common/acl/user/userMan.json" />';
|
var url = '<c:url value="/common/acl/user/userMan.json" />';
|
||||||
var url_view = '<c:url value="/common/acl/user/userMan.view" />';
|
var url_view = '<c:url value="/common/acl/user/userMan.view" />';
|
||||||
|
|
||||||
|
function userStatusFormat (cellvalue){
|
||||||
|
if ( cellvalue == '1' ){
|
||||||
|
return '<%= localeMessage.getString("code.normal") %>';
|
||||||
|
}else if ( cellvalue == '2' ) {
|
||||||
|
return '<span style="color:red"><%= localeMessage.getString("code.lock") %></span>';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||||
$('#grid').jqGrid({
|
$('#grid').jqGrid({
|
||||||
@@ -28,16 +38,20 @@ $(document).ready(function() {
|
|||||||
colNames:['<%= localeMessage.getString("login.userId") %>',
|
colNames:['<%= localeMessage.getString("login.userId") %>',
|
||||||
'<%= localeMessage.getString("user.name") %>',
|
'<%= localeMessage.getString("user.name") %>',
|
||||||
'<%= localeMessage.getString("user.roleName") %>',
|
'<%= localeMessage.getString("user.roleName") %>',
|
||||||
|
'<%= localeMessage.getString("user.status") %>',
|
||||||
'<%= localeMessage.getString("user.createOn") %>',
|
'<%= localeMessage.getString("user.createOn") %>',
|
||||||
|
'<%= localeMessage.getString("user.lastLoginYms") %>',
|
||||||
'<%= localeMessage.getString("user.depart") %>',
|
'<%= localeMessage.getString("user.depart") %>',
|
||||||
'<%= localeMessage.getString("user.teamName") %>',
|
'<%= localeMessage.getString("user.teamName") %>',
|
||||||
'<%= localeMessage.getString("user.position") %>'
|
'<%= localeMessage.getString("user.position") %>'
|
||||||
],
|
],
|
||||||
colModel:[
|
colModel:[
|
||||||
{ name : 'USERID' , align:'center' ,sortable:false },
|
{ name : 'USERID' , align:'center', width: 100, sortable:false },
|
||||||
{ name : 'USERNAME' , align:'center' },
|
{ name : 'USERNAME' , align:'center', width: 100 },
|
||||||
{ name : 'ROLEIDNFINAME' , align:'center' },
|
{ name : 'ROLEIDNFINAME' , align:'center', width: 300 },
|
||||||
{ name : 'LASTAMNDYMS' , align:'center', formatter: timeStampFormat },
|
{ name : 'STATUS' , align:'center', width: 80, formatter: userStatusFormat },
|
||||||
|
{ name : 'REGDYMS' , align:'center', width: 120, formatter: timeStampFormat },
|
||||||
|
{ name : 'LASTLOGINYMS' , align:'center', width: 120, formatter: timeStampFormat },
|
||||||
{ name : 'DVSNNAME' , align:'center', hidden: true },
|
{ name : 'DVSNNAME' , align:'center', hidden: true },
|
||||||
{ name : 'TEAMNAME' , align:'center', hidden: true },
|
{ name : 'TEAMNAME' , align:'center', hidden: true },
|
||||||
{ name : 'JOBTLNAME' , align:'center', hidden: true }
|
{ name : 'JOBTLNAME' , align:'center', hidden: true }
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ function detail(key){
|
|||||||
$(tag+"[name="+name+"]").val(data[name.toUpperCase()] ? data[name.toUpperCase()].trim():"");
|
$(tag+"[name="+name+"]").val(data[name.toUpperCase()] ? data[name.toUpperCase()].trim():"");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("input[name=LASTAMNDYMS]").inputmask("9999-99-99 99:99:99",{'autoUnmask':true});
|
$("input[name=REGDYMS]").inputmask("9999-99-99 99:99:99",{'autoUnmask':true});
|
||||||
|
$("input[name=LASTLOGINYMS]").inputmask("9999-99-99 99:99:99",{'autoUnmask':true});
|
||||||
/* $("input[name=SECONDMENTSTDT]").inputmask("9999-99-99",{'autoUnmask':true});
|
/* $("input[name=SECONDMENTSTDT]").inputmask("9999-99-99",{'autoUnmask':true});
|
||||||
$("input[name=SECONDMENTENDT]").inputmask("9999-99-99",{'autoUnmask':true}); */
|
$("input[name=SECONDMENTENDT]").inputmask("9999-99-99",{'autoUnmask':true}); */
|
||||||
|
|
||||||
@@ -216,6 +217,7 @@ $(document).ready(function() {
|
|||||||
dataType:"json",
|
dataType:"json",
|
||||||
data:{cmd: 'UPDATE_PASSWORD', userId : $("input[name=userId]").val()},
|
data:{cmd: 'UPDATE_PASSWORD', userId : $("input[name=userId]").val()},
|
||||||
success:function(json){
|
success:function(json){
|
||||||
|
$('select[name="status"]').val("1")
|
||||||
alert(json.result);
|
alert(json.result);
|
||||||
},
|
},
|
||||||
error:function(e){
|
error:function(e){
|
||||||
@@ -277,10 +279,10 @@ $(document).ready(function() {
|
|||||||
<table class="table_row" cellspacing="0">
|
<table class="table_row" cellspacing="0">
|
||||||
<input type="hidden" name="jobclcd">
|
<input type="hidden" name="jobclcd">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:180px;"><%= localeMessage.getString("login.userId") %></th><td ><input type="text" name="userId"/> </td>
|
<th style="width:180px;"><%= localeMessage.getString("login.userId") %> <span class=\"required-mark\">*</span></th><td ><input type="text" name="userId"/> </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%= localeMessage.getString("user.name") %> </th><td><input type="text" name="userName" /></td>
|
<th><%= localeMessage.getString("user.name") %> <span class=\"required-mark\">*</span></th><td><input type="text" name="userName" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="datailT">
|
<tr class="datailT">
|
||||||
<th><%= localeMessage.getString("user.auth") %> </th><td><input type="text" name="roleidnfiname" /></td>
|
<th><%= localeMessage.getString("user.auth") %> </th><td><input type="text" name="roleidnfiname" /></td>
|
||||||
@@ -317,7 +319,6 @@ $(document).ready(function() {
|
|||||||
<td>
|
<td>
|
||||||
<div class="select-style">
|
<div class="select-style">
|
||||||
<select name="status">
|
<select name="status">
|
||||||
<option value=""><%= localeMessage.getString("code.normal") %></option>
|
|
||||||
<option value="1"><%= localeMessage.getString("code.normal") %></option>
|
<option value="1"><%= localeMessage.getString("code.normal") %></option>
|
||||||
<option value="2"><%= localeMessage.getString("code.lock") %></option>
|
<option value="2"><%= localeMessage.getString("code.lock") %></option>
|
||||||
</select>
|
</select>
|
||||||
@@ -334,7 +335,10 @@ $(document).ready(function() {
|
|||||||
<th><%= localeMessage.getString("user.fieldAgentName") %> </th><td><input type="text" name="spotprxyname" /></td>
|
<th><%= localeMessage.getString("user.fieldAgentName") %> </th><td><input type="text" name="spotprxyname" /></td>
|
||||||
</tr> --%>
|
</tr> --%>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%= localeMessage.getString("user.createOn") %> </th><td><input type="text" name="LASTAMNDYMS" readonly/></td>
|
<th><%= localeMessage.getString("user.createOn") %> </th><td><input type="text" name="REGDYMS" readonly/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><%= localeMessage.getString("user.lastLoginYms") %> </th><td><input type="text" name="LASTLOGINYMS" readonly/></td>
|
||||||
</tr>
|
</tr>
|
||||||
<%-- <tr>
|
<%-- <tr>
|
||||||
<th><%= localeMessage.getString("user.secondmentbrncd") %> </th><td><input type="text" name="SECONDMENTBRNCD" readonly/></td>
|
<th><%= localeMessage.getString("user.secondmentbrncd") %> </th><td><input type="text" name="SECONDMENTBRNCD" readonly/></td>
|
||||||
|
|||||||
@@ -566,8 +566,8 @@
|
|||||||
</h1><!-- /monitoring/images/top_logo.png -->
|
</h1><!-- /monitoring/images/top_logo.png -->
|
||||||
<div class="topmenu_box">
|
<div class="topmenu_box">
|
||||||
<ul>
|
<ul>
|
||||||
<li style="width:240px;" id="newDashboardShow">
|
<li style="width:240px;">
|
||||||
<a style="width:240px;"
|
<a style="width:240px; cursor: default"
|
||||||
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>-<%=System.getProperty("inst.Name") %>)</span>
|
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>-<%=System.getProperty("inst.Name") %>)</span>
|
||||||
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
|
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
|
||||||
<% if (!"".equals(LastLoginYms.trim())) { %>
|
<% if (!"".equals(LastLoginYms.trim())) { %>
|
||||||
|
|||||||
@@ -82,8 +82,10 @@
|
|||||||
apiDesc: api.eaiSvcDesc
|
apiDesc: api.eaiSvcDesc
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
// addJSONData는 datatype:"local" 그리드의 p.data를 채우지 않아, 헤더 클릭 정렬 시 p.data(빈 배열) 기준으로 다시 그려지며 행이 사라짐.
|
||||||
|
// setGridParam({data:...}).trigger('reloadGrid')로 채워야 정렬이 정상 동작함.
|
||||||
$("#selectedApiGrid").jqGrid("clearGridData");
|
$("#selectedApiGrid").jqGrid("clearGridData");
|
||||||
$("#selectedApiGrid")[0].addJSONData(gridData);
|
$("#selectedApiGrid").jqGrid('setGridParam', {data: gridData}).trigger('reloadGrid');
|
||||||
|
|
||||||
var data = json;
|
var data = json;
|
||||||
$("#btn_gen_id").hide();
|
$("#btn_gen_id").hide();
|
||||||
@@ -133,7 +135,6 @@
|
|||||||
var selectedApiGrid = $("#selectedApiGrid");
|
var selectedApiGrid = $("#selectedApiGrid");
|
||||||
selectedApiGrid.jqGrid({
|
selectedApiGrid.jqGrid({
|
||||||
datatype: "local",
|
datatype: "local",
|
||||||
gridview: true, // 성능 향상
|
|
||||||
rownumbers: true, // 가상 인덱스
|
rownumbers: true, // 가상 인덱스
|
||||||
rownumWidth: 40,
|
rownumWidth: 40,
|
||||||
colNames: ['API ID', 'API 이름'],
|
colNames: ['API ID', 'API 이름'],
|
||||||
@@ -141,7 +142,7 @@
|
|||||||
{name: 'apiId', index: 'apiId', width: 100, align: 'center'},
|
{name: 'apiId', index: 'apiId', width: 100, align: 'center'},
|
||||||
{name: 'apiDesc', index: 'apiDesc', width: 100, align: 'center'}
|
{name: 'apiDesc', index: 'apiDesc', width: 100, align: 'center'}
|
||||||
],
|
],
|
||||||
rowNum: -1,
|
rowNum: 9999,
|
||||||
// rowList: [10, 20, 30],
|
// rowList: [10, 20, 30],
|
||||||
// pager: '#selectedApiGridPager',
|
// pager: '#selectedApiGridPager',
|
||||||
// sortname: 'apiId',
|
// sortname: 'apiId',
|
||||||
@@ -450,8 +451,8 @@
|
|||||||
</div><!-- end content_top -->
|
</div><!-- end content_top -->
|
||||||
<div class="content_middle">
|
<div class="content_middle">
|
||||||
<div class="search_wrap">
|
<div class="search_wrap">
|
||||||
<button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
<!-- <button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
||||||
</button>
|
</button> -->
|
||||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %>
|
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i
|
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ $(document).ready(function() {
|
|||||||
datatype:"json",
|
datatype:"json",
|
||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()},
|
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
|
||||||
colNames:['<%= localeMessage.getString("infAdpConMan.adaNm") %>',
|
colNames:['<%= localeMessage.getString("infAdpConMan.adaNm") %>',
|
||||||
'<%= localeMessage.getString("infAdpConMan.adaDes") %>',
|
'<%= localeMessage.getString("infAdpConMan.adaDes") %>',
|
||||||
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
|
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
|
||||||
@@ -62,6 +62,7 @@ $(document).ready(function() {
|
|||||||
url2 += '&menuId='+'${param.menuId}';
|
url2 += '&menuId='+'${param.menuId}';
|
||||||
//검색값
|
//검색값
|
||||||
url2 += '&searchName='+$("input[name=searchName]").val();
|
url2 += '&searchName='+$("input[name=searchName]").val();
|
||||||
|
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
|
||||||
//key값
|
//key값
|
||||||
url2 += '&name='+name;
|
url2 += '&name='+name;
|
||||||
goNav(url2);
|
goNav(url2);
|
||||||
@@ -80,7 +81,7 @@ $(document).ready(function() {
|
|||||||
resizeJqGridWidth('grid','content_middle','1000');
|
resizeJqGridWidth('grid','content_middle','1000');
|
||||||
|
|
||||||
$("#btn_search").click(function(){
|
$("#btn_search").click(function(){
|
||||||
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid");
|
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
|
||||||
});
|
});
|
||||||
$("#btn_new").click(function(){
|
$("#btn_new").click(function(){
|
||||||
var url2 = url_view;
|
var url2 = url_view;
|
||||||
@@ -126,6 +127,16 @@ $(document).ready(function() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
|
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
|
||||||
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
||||||
|
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
|
||||||
|
<td>
|
||||||
|
<div class="select-style">
|
||||||
|
<select name="searchUseYn">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="1">사용함</option>
|
||||||
|
<option value="0">사용안함</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ $(document).ready(function() {
|
|||||||
datatype:"json",
|
datatype:"json",
|
||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()},
|
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
|
||||||
colNames:['클라이언트 ID',
|
colNames:['클라이언트 ID',
|
||||||
'클라이언트명',
|
'클라이언트명',
|
||||||
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
|
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
|
||||||
@@ -62,6 +62,7 @@ $(document).ready(function() {
|
|||||||
url2 += '&menuId='+'${param.menuId}';
|
url2 += '&menuId='+'${param.menuId}';
|
||||||
//검색값
|
//검색값
|
||||||
url2 += '&searchName='+$("input[name=searchName]").val();
|
url2 += '&searchName='+$("input[name=searchName]").val();
|
||||||
|
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
|
||||||
//key값
|
//key값
|
||||||
url2 += '&name='+name;
|
url2 += '&name='+name;
|
||||||
goNav(url2);
|
goNav(url2);
|
||||||
@@ -80,7 +81,7 @@ $(document).ready(function() {
|
|||||||
resizeJqGridWidth('grid','content_middle','1000');
|
resizeJqGridWidth('grid','content_middle','1000');
|
||||||
|
|
||||||
$("#btn_search").click(function(){
|
$("#btn_search").click(function(){
|
||||||
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid");
|
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
|
||||||
});
|
});
|
||||||
$("#btn_new").click(function(){
|
$("#btn_new").click(function(){
|
||||||
var url2 = url_view;
|
var url2 = url_view;
|
||||||
@@ -90,6 +91,7 @@ $(document).ready(function() {
|
|||||||
url2 += '&menuId='+'${param.menuId}';
|
url2 += '&menuId='+'${param.menuId}';
|
||||||
//검색값
|
//검색값
|
||||||
url2 += '&searchName=';
|
url2 += '&searchName=';
|
||||||
|
url2 += '&searchUseYn='+$('select[name=searchUseYn]').val();
|
||||||
|
|
||||||
goNav(url2);
|
goNav(url2);
|
||||||
});
|
});
|
||||||
@@ -124,6 +126,16 @@ $(document).ready(function() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width:180px;">클라이언트명</th>
|
<th style="width:180px;">클라이언트명</th>
|
||||||
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
||||||
|
<th style="width:180px;">사용여부</th>
|
||||||
|
<td>
|
||||||
|
<div class="select-style">
|
||||||
|
<select name="searchUseYn">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="1">사용함</option>
|
||||||
|
<option value="0">사용안함</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ $(document).ready(function() {
|
|||||||
url: url,
|
url: url,
|
||||||
data: postData,
|
data: postData,
|
||||||
success: function(args) {
|
success: function(args) {
|
||||||
|
console.log('mod', args);
|
||||||
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
|
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: '저장 완료',
|
title: '저장 완료',
|
||||||
@@ -493,11 +494,6 @@ $(document).ready(function() {
|
|||||||
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
|
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
|
||||||
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
|
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ function groupNameFormat(cellvalue, options, rowObject) {
|
|||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$("input[name=searchStartYYYYMMDD],input[name=searchEndYYYYMMDD]").inputmask("yyyy-mm-dd",{'autoUnmask':true});
|
$("input[name=searchStartYYYYMMDD],input[name=searchEndYYYYMMDD]").inputmask("yyyy-mm-dd",{'autoUnmask':true});
|
||||||
|
$("input[name=searchStartTime],input[name=searchEndTime]").inputmask("hh:mm:ss",{'autoUnmask':true});
|
||||||
|
|
||||||
$("input[name=searchStartYYYYMMDD]").each(function(){
|
$("input[name=searchStartYYYYMMDD]").each(function(){
|
||||||
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
|
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
|
||||||
@@ -62,6 +63,18 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("input[name=searchStartTime]").each(function(){
|
||||||
|
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
|
||||||
|
$(this).val("000000");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input[name=searchEndTime]").each(function(){
|
||||||
|
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
|
||||||
|
$(this).val("235959");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||||
|
|
||||||
@@ -136,8 +149,10 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi,"");
|
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi,"");
|
||||||
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi,"");
|
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi,"");
|
||||||
|
var startTime = $("input[name=searchStartTime]").val();
|
||||||
|
var endTime = $("input[name=searchEndTime]").val();
|
||||||
|
|
||||||
if(start > end){
|
if((start + startTime) > (end + endTime)){
|
||||||
alert("조회기간을 확인해주세요.");
|
alert("조회기간을 확인해주세요.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -197,14 +212,24 @@ $(document).ready(function() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.date") %></th>
|
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.date") %></th>
|
||||||
<td>
|
<td colspan="5">
|
||||||
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:80px; border:1px solid #ebebec;">
|
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:80px; border:1px solid #ebebec;">
|
||||||
|
<input type="text" name="searchStartTime" value="" size="8" style="width:70px; border:1px solid #ebebec;">
|
||||||
~
|
~
|
||||||
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:80px; border:1px solid #ebebec;">
|
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:80px; border:1px solid #ebebec;">
|
||||||
|
<input type="text" name="searchEndTime" value="" size="8" style="width:70px; border:1px solid #ebebec;">
|
||||||
<input type="hidden" name="searchStartDate" value="" style="width:0px;">
|
<input type="hidden" name="searchStartDate" value="" style="width:0px;">
|
||||||
<input type="hidden" name="searchEndDate" value="" style="width:0px;">
|
<input type="hidden" name="searchEndDate" value="" style="width:0px;">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.instNm") %></th>
|
||||||
|
<td><input type="text" name="searchInstanceName" value="${param.searchInstanceName}"></td>
|
||||||
|
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.eaiSvcCd") %></th>
|
||||||
|
<td><input type="text" name="searchApiName" value="${param.searchApiName}"></td>
|
||||||
|
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.adpGrpNm") %></th>
|
||||||
|
<td><input type="text" name="searchAdapterGroupName" value="${param.searchAdapterGroupName}"></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ $(document).ready(function() {
|
|||||||
datatype:"json",
|
datatype:"json",
|
||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
postData : { cmd : 'LIST', searchGroupName: $('input[name=searchGroupName]').val()},
|
postData : { cmd : 'LIST', searchGroupName: $('input[name=searchGroupName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
|
||||||
colNames:['그룹 ID',
|
colNames:['그룹 ID',
|
||||||
'그룹명',
|
'그룹명',
|
||||||
'초당 임계치',
|
'초당 임계치',
|
||||||
@@ -125,6 +125,7 @@ $(document).ready(function() {
|
|||||||
url2 += '&menuId='+'${param.menuId}';
|
url2 += '&menuId='+'${param.menuId}';
|
||||||
//검색값
|
//검색값
|
||||||
url2 += '&searchGroupName='+$("input[name=searchGroupName]").val();
|
url2 += '&searchGroupName='+$("input[name=searchGroupName]").val();
|
||||||
|
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
|
||||||
//key값
|
//key값
|
||||||
url2 += '&groupId='+groupId;
|
url2 += '&groupId='+groupId;
|
||||||
goNav(url2);
|
goNav(url2);
|
||||||
@@ -143,7 +144,7 @@ $(document).ready(function() {
|
|||||||
resizeJqGridWidth('grid','content_middle','1000');
|
resizeJqGridWidth('grid','content_middle','1000');
|
||||||
|
|
||||||
$("#btn_search").click(function(){
|
$("#btn_search").click(function(){
|
||||||
$("#grid").setGridParam({ postData: { searchGroupName: $('input[name=searchGroupName]').val()}, page:1 }).trigger("reloadGrid");
|
$("#grid").setGridParam({ postData: { searchGroupName: $('input[name=searchGroupName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
|
||||||
});
|
});
|
||||||
$("#btn_new").click(function(){
|
$("#btn_new").click(function(){
|
||||||
var url2 = url_view;
|
var url2 = url_view;
|
||||||
@@ -188,6 +189,16 @@ $(document).ready(function() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width:180px;">그룹명</th>
|
<th style="width:180px;">그룹명</th>
|
||||||
<td><input type="text" name="searchGroupName" value="${param.searchGroupName}"></td>
|
<td><input type="text" name="searchGroupName" value="${param.searchGroupName}"></td>
|
||||||
|
<th style="width:180px;">사용여부</th>
|
||||||
|
<td>
|
||||||
|
<div class="select-style">
|
||||||
|
<select name="searchUseYn">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="1">사용함</option>
|
||||||
|
<option value="0">사용안함</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ $(document).ready(function() {
|
|||||||
datatype:"json",
|
datatype:"json",
|
||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()},
|
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
|
||||||
colNames:['<%= localeMessage.getString("infIfConMan.ifNm") %>',
|
colNames:['<%= localeMessage.getString("infIfConMan.ifNm") %>',
|
||||||
'<%= localeMessage.getString("infIfConMan.ifDesc") %>',
|
'<%= localeMessage.getString("infIfConMan.ifDesc") %>',
|
||||||
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
|
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
|
||||||
@@ -62,6 +62,7 @@ $(document).ready(function() {
|
|||||||
url2 += '&menuId='+'${param.menuId}';
|
url2 += '&menuId='+'${param.menuId}';
|
||||||
//검색값
|
//검색값
|
||||||
url2 += '&searchName='+$("input[name=searchName]").val();
|
url2 += '&searchName='+$("input[name=searchName]").val();
|
||||||
|
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
|
||||||
//key값
|
//key값
|
||||||
url2 += '&name='+name;
|
url2 += '&name='+name;
|
||||||
goNav(url2);
|
goNav(url2);
|
||||||
@@ -80,7 +81,7 @@ $(document).ready(function() {
|
|||||||
resizeJqGridWidth('grid','content_middle','1000');
|
resizeJqGridWidth('grid','content_middle','1000');
|
||||||
|
|
||||||
$("#btn_search").click(function(){
|
$("#btn_search").click(function(){
|
||||||
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid");
|
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
|
||||||
});
|
});
|
||||||
$("#btn_new").click(function(){
|
$("#btn_new").click(function(){
|
||||||
var url2 = url_view;
|
var url2 = url_view;
|
||||||
@@ -126,6 +127,16 @@ $(document).ready(function() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width:180px;"><%= localeMessage.getString("infIfConMan.ifNm") %></th>
|
<th style="width:180px;"><%= localeMessage.getString("infIfConMan.ifNm") %></th>
|
||||||
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
||||||
|
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
|
||||||
|
<td>
|
||||||
|
<div class="select-style">
|
||||||
|
<select name="searchUseYn">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="1">사용함</option>
|
||||||
|
<option value="0">사용안함</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -76,13 +76,13 @@
|
|||||||
columns = [ // for columnData prop
|
columns = [ // for columnData prop
|
||||||
{ title: '<%= localeMessage.getString("layoutMan.itemDesc") %>', name: 'loutItemDesc', type: 'text', width:280, resize:true, minWidth:120 },
|
{ title: '<%= localeMessage.getString("layoutMan.itemDesc") %>', name: 'loutItemDesc', type: 'text', width:280, resize:true, minWidth:120 },
|
||||||
{ title: '<%= localeMessage.getString("layoutMan.itmeEn") %>', name: 'loutItemName', type: 'text', width:280, resize:true, minWidth:120 },
|
{ title: '<%= localeMessage.getString("layoutMan.itmeEn") %>', name: 'loutItemName', type: 'text', width:280, resize:true, minWidth:120 },
|
||||||
{ title: '<%=localeMessage.getString("standardLayout.itemLevel")%>', name: 'loutItemDepth', type: 'text', width:50 },
|
{ title: '<%=localeMessage.getString("standardLayout.itemLevel")%>', name: 'loutItemDepth', type: 'numeric', width:50 },
|
||||||
{ title: '<%= localeMessage.getString("standardLayout.itemType") %>', name: 'loutItemType', type: 'autocomplete', source: itemTypes, width:70 },
|
{ title: '<%= localeMessage.getString("standardLayout.itemType") %>', name: 'loutItemType', type: 'autocomplete', source: itemTypes, width:70 },
|
||||||
{ title: '<%= localeMessage.getString("standardLayout.arraySize") %>', name: 'loutItemOccCnt', type: 'text', width:90 },
|
{ title: '<%= localeMessage.getString("standardLayout.arraySize") %>', name: 'loutItemOccCnt', type: 'text', width:90 },
|
||||||
{ title: '<%= localeMessage.getString("standardLayout.refField") %>', name: 'loutItemOccRef', type: 'text', width:90 },
|
{ title: '<%= localeMessage.getString("standardLayout.refField") %>', name: 'loutItemOccRef', type: 'text', width:90 },
|
||||||
{ title: '<%=localeMessage.getString("layoutMan.dataType")%>', name: 'loutItemDataType', type: 'autocomplete', source: basicDataTypes, width:160 },
|
{ title: '<%=localeMessage.getString("layoutMan.dataType")%>', name: 'loutItemDataType', type: 'autocomplete', source: basicDataTypes, width:160 },
|
||||||
{ title: '<%=localeMessage.getString("layoutMan.dataLen")%>', name: 'loutItemLength', type: 'numeric', width: 70, align: 'center' },
|
{ title: '<%=localeMessage.getString("layoutMan.dataLen")%>', name: 'loutItemLength', type: 'numeric', width: 70, align: 'center' },
|
||||||
{ title: '<%=localeMessage.getString("layoutMan.decpLen")%>', name: 'loutItemDecimal', type: 'text', width: 70 },
|
{ title: '<%=localeMessage.getString("layoutMan.decpLen")%>', name: 'loutItemDecimal', type: 'numeric', width: 70 },
|
||||||
{ title: '<%=localeMessage.getString("layoutMan.defaultVal")%>', name: 'loutItemDefault', type: 'text', width: 70 },
|
{ title: '<%=localeMessage.getString("layoutMan.defaultVal")%>', name: 'loutItemDefault', type: 'text', width: 70 },
|
||||||
{ title: '<%=localeMessage.getString("layoutMan.mask")%>', name: 'loutItemMaskYn', type: 'checkbox', width: 70 },
|
{ title: '<%=localeMessage.getString("layoutMan.mask")%>', name: 'loutItemMaskYn', type: 'checkbox', width: 70 },
|
||||||
{ title: '<%=localeMessage.getString("layoutMan.moffset")%>', name: 'loutItemMaskOffset', type: 'text', width: 50 },
|
{ title: '<%=localeMessage.getString("layoutMan.moffset")%>', name: 'loutItemMaskOffset', type: 'text', width: 50 },
|
||||||
@@ -182,6 +182,41 @@
|
|||||||
$('#dataTotalLen').text(totalLen);
|
$('#dataTotalLen').text(totalLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusGridCell(colIndex, rowIndex) {
|
||||||
|
var cellName = jexcel.getColumnNameFromId([colIndex, rowIndex]);
|
||||||
|
var cell = grid.getCell(cellName);
|
||||||
|
grid.updateSelection(cell);
|
||||||
|
cell.scrollIntoView({block: 'center', inline: 'center'});
|
||||||
|
grid.openEditor(cell, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var INTEGER_REGEXP = /^(0|[1-9][0-9]*)$/;
|
||||||
|
|
||||||
|
function validateNumberColumns() {
|
||||||
|
var data = grid.getData();
|
||||||
|
var gridColumns = grid.options.columns;
|
||||||
|
|
||||||
|
for (var col = 0; col < gridColumns.length; col++) {
|
||||||
|
if (gridColumns[col].type !== 'numeric') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var row = 0; row < data.length; row++) {
|
||||||
|
var value = data[row][col];
|
||||||
|
if (value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
value = $.trim(String(value));
|
||||||
|
if (value !== '' && !INTEGER_REGEXP.test(value)) {
|
||||||
|
alert("숫자만 입력할 수 있습니다.");
|
||||||
|
focusGridCell(col, row);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function detail(key) {
|
function detail(key) {
|
||||||
if (!isDetail) {
|
if (!isDetail) {
|
||||||
return
|
return
|
||||||
@@ -321,6 +356,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#btn_modify").click(function(){
|
$("#btn_modify").click(function(){
|
||||||
|
if (!validateNumberColumns()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var postData = $('#ajaxForm').serializeArray();
|
var postData = $('#ajaxForm').serializeArray();
|
||||||
if (isDetail){
|
if (isDetail){
|
||||||
postData.push({ name: "cmd" , value:"UPDATE"});
|
postData.push({ name: "cmd" , value:"UPDATE"});
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||||
|
<%@ page import="java.io.*"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||||
|
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||||
|
<%
|
||||||
|
response.setHeader("Pragma", "No-cache");
|
||||||
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
|
||||||
|
%>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
|
||||||
|
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||||
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
|
<script language="javascript" >
|
||||||
|
var url = '<c:url value="/onl/transaction/apim/apiInterfaceMan.json"/>';
|
||||||
|
var url_view = '<c:url value="/onl/transaction/apim/apiInterfaceMan.view"/>';
|
||||||
|
|
||||||
|
var selectName = "searchEaiBzwkDstcd"; // selectBox Name
|
||||||
|
function processSelectedData() {
|
||||||
|
var grid = $("#grid");
|
||||||
|
var selectedRowIds = grid.jqGrid('getGridParam', 'selarrrow');
|
||||||
|
var selectedData = [];
|
||||||
|
|
||||||
|
for(var i = 0; i < selectedRowIds.length; i++) {
|
||||||
|
var rowData = grid.jqGrid('getRowData', selectedRowIds[i]);
|
||||||
|
selectedData.push({
|
||||||
|
bizCode: rowData.eaiBzwkDstcd, // CLINET
|
||||||
|
apiId: rowData.eaiSvcName,
|
||||||
|
apiDesc: rowData.eaiSvcDesc, // SCOPE-API
|
||||||
|
APIFULLPATH: rowData.apiFullPath,
|
||||||
|
BZWKSVCKEYNAME: rowData.bzwksvckeyname
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(selectedData.length === 0) {
|
||||||
|
alert("선택된 항목이 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("선택한 " + selectedData.length + "건의 API를 추가합니다.");
|
||||||
|
window.returnValue = selectedData.length === 1 ? selectedData[0] : selectedData;
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectionInfo(gridId) {
|
||||||
|
var grid = $(gridId || "#grid");
|
||||||
|
var selectedRows = grid.jqGrid('getGridParam', 'selarrrow');
|
||||||
|
$("#selected-count").text("총: " + selectedRows.length + " 건");
|
||||||
|
}
|
||||||
|
|
||||||
|
function init( callback) {
|
||||||
|
$.ajax({
|
||||||
|
type : "POST",
|
||||||
|
url:url,
|
||||||
|
dataType:"json",
|
||||||
|
data:{cmd: 'LIST_COMBO'},
|
||||||
|
success:function(json){
|
||||||
|
new makeOptions("BIZCODE","BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("","전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering();
|
||||||
|
|
||||||
|
setSearchable(selectName); // 콤보에 searchable 설정
|
||||||
|
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error:function(e){
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
function detail(){
|
||||||
|
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||||
|
$("#grid").setGridParam({ url:url,postData: postData }).trigger("reloadGrid");
|
||||||
|
}
|
||||||
|
|
||||||
|
function search(){
|
||||||
|
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||||
|
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
|
||||||
|
}
|
||||||
|
function ioFormatter(cellvalue,options,rowObject){
|
||||||
|
var serviceType = sessionStorage["serviceType"];
|
||||||
|
if(cellvalue == "I"){
|
||||||
|
return "타발";
|
||||||
|
}else if(cellvalue =="O"){
|
||||||
|
return "당발";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function adapterNameShortFormatter(cellvalue,options,rowObject){
|
||||||
|
if(cellvalue == null || cellvalue == '')
|
||||||
|
return ''
|
||||||
|
return cellvalue.substring(1, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function list(){
|
||||||
|
detail()
|
||||||
|
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||||
|
// gridPostData.searchApiEnableYn = 'Y';
|
||||||
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
|
var selectedApiIds = urlParams.get("selectedApiIds");
|
||||||
|
$('#grid').jqGrid({
|
||||||
|
datatype : "json",
|
||||||
|
mtype : 'POST',
|
||||||
|
url : url,
|
||||||
|
postData : gridPostData,
|
||||||
|
colNames : [ '업무구분',
|
||||||
|
'<%= localeMessage.getString("eaiMessage.eaiSvcName")%>',
|
||||||
|
'<%= localeMessage.getString("eaiMessage.eaiSvcDesc")%>',
|
||||||
|
'API FULL PATH',
|
||||||
|
'업무서비스명',
|
||||||
|
'요청',
|
||||||
|
'응답',
|
||||||
|
'작성자',
|
||||||
|
'가상응답여부',
|
||||||
|
'SyncAsyncType',
|
||||||
|
],
|
||||||
|
colModel : [ { name : 'eaiBzwkDstcd' , align : 'center' , width:'40', sortable:false},
|
||||||
|
{ name : 'eaiSvcName' , align : 'left' , width:'100'},
|
||||||
|
{ name : 'eaiSvcDesc' , align : 'left' },
|
||||||
|
{ name : 'apiFullPath' , align : 'left' , width:'100'},
|
||||||
|
{ name : 'bzwksvckeyname' , align : 'left' , width:'100', hidden: true},
|
||||||
|
{ name : 'fromAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
|
||||||
|
{ name : 'toAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
|
||||||
|
{ name : 'author' , align : 'center' , width:'60' },
|
||||||
|
{ name : 'simYn' , align : 'center' , width:'40' },
|
||||||
|
{ name : 'syncAsyncType' , align : 'center' , width:'40', hidden: true},
|
||||||
|
],
|
||||||
|
jsonReader : {
|
||||||
|
repeatitems : false
|
||||||
|
},
|
||||||
|
pager : $('#pager'),
|
||||||
|
page : '${param.page}',
|
||||||
|
rowNum : '${rmsDefaultRowNum}',
|
||||||
|
autoheight : true,
|
||||||
|
height : $("#container").height(),
|
||||||
|
autowidth : true,
|
||||||
|
viewrecords : true,
|
||||||
|
multiselect: false,
|
||||||
|
multiboxonly: false,
|
||||||
|
rowList : eval('[${rmsDefaultRowList}]'),
|
||||||
|
loadComplete:function (d){
|
||||||
|
var $grid = $(this);
|
||||||
|
if(selectedApiIds) {
|
||||||
|
const selectedIds = selectedApiIds.split(',');
|
||||||
|
$.each($grid.getDataIDs(), function(_, id) {
|
||||||
|
var rowData = $grid.getRowData(id);
|
||||||
|
if(selectedIds.includes(rowData.eaiSvcName)) {
|
||||||
|
$grid.jqGrid('setSelection', id, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var colModel = $(this).getGridParam("colModel");
|
||||||
|
for(var i = 0 ; i< colModel.length; i++){
|
||||||
|
$(this).setColProp(colModel[i].name, {sortable : false});
|
||||||
|
}
|
||||||
|
updateSelectionInfo("#grid");
|
||||||
|
},
|
||||||
|
onSelectRow: function(rowid, status, e) {
|
||||||
|
updateSelectionInfo("#grid");
|
||||||
|
},
|
||||||
|
onSelectAll: function(rowids, status) {
|
||||||
|
updateSelectionInfo("#grid");
|
||||||
|
},
|
||||||
|
ondblClickRow : function(rowId) {
|
||||||
|
const rowData = $(this).getRowData(rowId);
|
||||||
|
const returnValue = {
|
||||||
|
bizCode: rowData.eaiBzwkDstcd,
|
||||||
|
apiId: rowData.eaiSvcName,
|
||||||
|
apiDesc: rowData.eaiSvcDesc
|
||||||
|
};
|
||||||
|
//alert("선택한 API를 추가합니다.\nAPI ID: " + rowData.eaiSvcName);
|
||||||
|
window.returnValue = returnValue;
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
init(list);
|
||||||
|
|
||||||
|
resizeJqGridWidth('grid','content_middle','1000');
|
||||||
|
|
||||||
|
// 업무구분명 선택 시 자동 검색
|
||||||
|
$("select[name=searchEaiBzwkDstcd]").change(function() {
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
$("#btn_search").click(function(){
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
$("input[name^=search]").keydown(function(key){
|
||||||
|
if (key.keyCode == 13){
|
||||||
|
$("#btn_search").click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonControl();
|
||||||
|
|
||||||
|
$("#btn_select").click(function(){
|
||||||
|
processSelectedData();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btn_close").click(function () {
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div class="content_middle" id="content_middle" style="margin-top: 0px">
|
||||||
|
<div class="search_wrap">
|
||||||
|
<button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_select" level="R" ><i class="material-icons">check</i> <%= localeMessage.getString("button.check") %></button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_close" level="R" status="DETAIL"><i class="material-icons">close</i> <%= localeMessage.getString("button.close") %></button>
|
||||||
|
</div>
|
||||||
|
<div class="title" id="title" >${rmsMenuName}</div>
|
||||||
|
<!-- 선택 정보 표시 영역 추가 -->
|
||||||
|
<div style="margin-bottom: 5px;">
|
||||||
|
<span id="selected-count" style="color: #666; font-size: 12px;"></span>
|
||||||
|
</div>
|
||||||
|
<table class="search_condition" cellspacing=0;>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style="width:180px;">업무구분명</th>
|
||||||
|
<td>
|
||||||
|
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}">
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}">
|
||||||
|
</td>
|
||||||
|
<th style="width:180px;">API FULL PATH</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table id="grid" ></table>
|
||||||
|
<div id="pager"></div>
|
||||||
|
</div><!-- end content_middle -->
|
||||||
|
</div><!-- end right_box -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -1947,8 +1947,8 @@
|
|||||||
var key = "";
|
var key = "";
|
||||||
var args = new Object();
|
var args = new Object();
|
||||||
args['eaiSvcName'] = $('input[name=eaiSvcName]').val();
|
args['eaiSvcName'] = $('input[name=eaiSvcName]').val();
|
||||||
var url='<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
var url=url_view; //'<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
||||||
url = url + "?cmd=POPUP";
|
url = url + "?cmd=API_POPUP";
|
||||||
var ret = showModal(url,args,1020,630, function(arg){
|
var ret = showModal(url,args,1020,630, function(arg){
|
||||||
var args = null;
|
var args = null;
|
||||||
if(arg == null || arg == undefined ) {//chrome
|
if(arg == null || arg == undefined ) {//chrome
|
||||||
@@ -1962,11 +1962,11 @@
|
|||||||
|
|
||||||
var ret = args.returnValue;
|
var ret = args.returnValue;
|
||||||
console.log("ret",ret);
|
console.log("ret",ret);
|
||||||
key = ret['key'];
|
key = ret['apiId'];
|
||||||
|
|
||||||
$("input[name=eaiSvcName]").val(key);
|
$("input[name=eaiSvcName]").val(key);
|
||||||
$("input[name=eaiSvcDesc]").val(ret['eaiSvcDesc']);
|
$("input[name=eaiSvcDesc]").val(ret['apiDesc']);
|
||||||
bzwkDstCd = ret['eaiBzwkDstCd'];
|
bzwkDstCd = ret['bizCode'];
|
||||||
|
|
||||||
$("input[name=eaiSvcName]").change();
|
$("input[name=eaiSvcName]").change();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
|
|
||||||
<script language="javascript" >
|
<script language="javascript" >
|
||||||
|
var url_view = '<c:url value="/onl/admin/rule/transform2/transform2Man.view" />';
|
||||||
var $ = jQuery.noConflict();
|
var $ = jQuery.noConflict();
|
||||||
var bzwkDstCd = "";
|
var bzwkDstCd = "";
|
||||||
|
|
||||||
@@ -81,8 +82,9 @@ $(document).ready(function() {
|
|||||||
var key = "";
|
var key = "";
|
||||||
var args = new Object();
|
var args = new Object();
|
||||||
args['eaiSvcName'] = $('input[name=eaiSvcName]').val();
|
args['eaiSvcName'] = $('input[name=eaiSvcName]').val();
|
||||||
var url='<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
var url=url_view; //'<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
||||||
url = url + "?cmd=POPUP";
|
url = url + "?cmd=API_POPUP";
|
||||||
|
console.log('[new]', url);
|
||||||
var ret = showModal(url,args,1020,630, function(arg){
|
var ret = showModal(url,args,1020,630, function(arg){
|
||||||
var args = null;
|
var args = null;
|
||||||
if(arg == null || arg == undefined ) {//chrome
|
if(arg == null || arg == undefined ) {//chrome
|
||||||
@@ -96,11 +98,11 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
var ret = args.returnValue;
|
var ret = args.returnValue;
|
||||||
console.log("ret",ret);
|
console.log("ret",ret);
|
||||||
key = ret['key'];
|
key = ret['apiId'];
|
||||||
|
|
||||||
$("input[name=eaiSvcName]").val(key);
|
$("input[name=eaiSvcName]").val(key);
|
||||||
$("input[name=eaiSvcDesc]").val(ret['eaiSvcDesc']);
|
$("input[name=eaiSvcDesc]").val(ret['apiDesc']);
|
||||||
bzwkDstCd = ret['eaiBzwkDstCd'];
|
bzwkDstCd = ret['bizCode'];
|
||||||
|
|
||||||
$("input[name=eaiSvcName]").change();
|
$("input[name=eaiSvcName]").change();
|
||||||
//$("select[name=eaiSevrDstcd]").val(ret['eaiSevrDstcd']);
|
//$("select[name=eaiSevrDstcd]").val(ret['eaiSevrDstcd']);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<script language="javascript" >
|
<script language="javascript" >
|
||||||
var url = '<c:url value="/onl/apim/apigroup/apiGroupMan.json"/>';
|
var url = '<c:url value="/onl/apim/apigroup/apiGroupMan.json"/>';
|
||||||
var url_view = '<c:url value="/onl/apim/apigroup/apiGroupMan.view"/>';
|
var url_view = '<c:url value="/onl/apim/apigroup/apiGroupMan.view"/>';
|
||||||
var url_popup_view = '<c:url value="/onl/transaction/apim/apiSpecManPopup.view"/>';
|
var url_popup_view = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
|
||||||
|
|
||||||
var isDetail = false;
|
var isDetail = false;
|
||||||
let dialog;
|
let dialog;
|
||||||
@@ -415,6 +415,12 @@
|
|||||||
<input type="text" name="groupName" data-required data-warning="API 그룹 명을 입력하여 주십시오.">
|
<input type="text" name="groupName" data-required data-warning="API 그룹 명을 입력하여 주십시오.">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:150px;">그룹 설명</th>
|
||||||
|
<td colspan="7">
|
||||||
|
<input type="text" name="groupDesc">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:150px;">공개 여부</th>
|
<th style="width:150px;">공개 여부</th>
|
||||||
<td style="width:100px;">
|
<td style="width:100px;">
|
||||||
@@ -461,14 +467,6 @@
|
|||||||
<img id="mainIconPreview" src="#" alt="Main Icon Preview" style="display:none;height:20px">
|
<img id="mainIconPreview" src="#" alt="Main Icon Preview" style="display:none;height:20px">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
<%--<tr>
|
|
||||||
<th>컨텐츠 내용</th>
|
|
||||||
<td colspan="5">
|
|
||||||
<textarea name="contentDetail" id="contentDetail" rows="10" cols="50"></textarea>
|
|
||||||
</td>
|
|
||||||
</tr>--%>
|
|
||||||
</table>
|
</table>
|
||||||
<div style="margin-top:15px;">
|
<div style="margin-top:15px;">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px ">
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px ">
|
||||||
|
|||||||
@@ -56,11 +56,16 @@
|
|||||||
$("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
|
$("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
|
||||||
|
|
||||||
$("#btn_app_approve").click(function () {
|
$("#btn_app_approve").click(function () {
|
||||||
if (confirm("승인하시겠습니까?")) {
|
var isDeleteReq = (currentRequestType === 'DELETE');
|
||||||
|
var gwAction = (isDeleteReq && $('#chkGwDelete').is(':checked')) ? 'DELETE' : 'BLOCK';
|
||||||
|
var confirmMsg = isDeleteReq
|
||||||
|
? "해지 승인 시 포털 인증키(Credential)는 삭제되며,\n게이트웨이 클라이언트는 [" + (gwAction === 'DELETE' ? "완전 삭제" : "차단(비활성화)") + "] 처리됩니다.\n승인하시겠습니까?"
|
||||||
|
: "승인하시겠습니까?";
|
||||||
|
if (confirm(confirmMsg)) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
url: url,
|
url: url,
|
||||||
data: {cmd: "APPROVE", id: key},
|
data: {cmd: "APPROVE", id: key, gwAction: gwAction},
|
||||||
success: function (args) {
|
success: function (args) {
|
||||||
<%--alert("<%= localeMessage.getString("approval.approveSuccess") %>");--%>
|
<%--alert("<%= localeMessage.getString("approval.approveSuccess") %>");--%>
|
||||||
alert("승인되었습니다.");
|
alert("승인되었습니다.");
|
||||||
@@ -101,6 +106,32 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btn_app_force_reject").click(function () {
|
||||||
|
if (!confirm("승인자 목록에 삭제된 사용자가 있어 승인 진행이 불가능한 건입니다.\n강제 반려하시겠습니까?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rejectReason = prompt("강제 반려 사유를 입력해 주세요");
|
||||||
|
if (rejectReason != null) {
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: url,
|
||||||
|
data: {cmd: "FORCE_REJECT", id: key, rejectReason: rejectReason},
|
||||||
|
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_app_redeploy').click(function () {
|
$('#btn_app_redeploy').click(function () {
|
||||||
if (confirm("재배포 하시겠습니까?")) {
|
if (confirm("재배포 하시겠습니까?")) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -150,6 +181,8 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var currentRequestType = null; // 현재 신청 유형 (DELETE 승인 시 GW 처리방식 선택 노출용)
|
||||||
|
|
||||||
function detail(key) {
|
function detail(key) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
@@ -178,10 +211,11 @@
|
|||||||
$("#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);
|
$("#expectEndDate").val(data.expectEndDate);
|
||||||
|
|
||||||
if (data.approvers.some(approver =>
|
var isCurrentApprover = data.approvers.some(approver =>
|
||||||
approver.approverStatus === 'CURRENT' &&
|
approver.approverStatus === 'CURRENT' &&
|
||||||
approver.user.USERID === loginUser
|
approver.user && approver.user.USERID === loginUser
|
||||||
)) {
|
);
|
||||||
|
if (isCurrentApprover) {
|
||||||
document.getElementById("btn_app_approve").style.display = "";
|
document.getElementById("btn_app_approve").style.display = "";
|
||||||
document.getElementById("btn_app_reject").style.display = "";
|
document.getElementById("btn_app_reject").style.display = "";
|
||||||
} else {
|
} else {
|
||||||
@@ -189,6 +223,29 @@
|
|||||||
document.getElementById("btn_app_reject").style.display = "none";
|
document.getElementById("btn_app_reject").style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API 이용해지(DELETE) 건: 승인 가능 상태의 현재 승인자에게만 GW 처리방식 선택 노출
|
||||||
|
currentRequestType = data.requestType;
|
||||||
|
var inProgress = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
|
||||||
|
if (data.requestType === 'DELETE' && isCurrentApprover && inProgress) {
|
||||||
|
$("#gwActionSection").show();
|
||||||
|
} else {
|
||||||
|
$("#gwActionSection").hide();
|
||||||
|
}
|
||||||
|
// 처리 완료된 해지 건은 어떤 방식으로 실행됐는지 표시
|
||||||
|
if (data.requestType === 'DELETE' && data.gwAction) {
|
||||||
|
$("#gwActionResult").text("GW 처리방식: " + (data.gwAction === 'DELETE' ? "완전 삭제" : "차단(비활성화)")).show();
|
||||||
|
} else {
|
||||||
|
$("#gwActionResult").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 삭제된 승인자(USERID 없음)로 진행 불가한 건: 요청됨/진행중 상태면 승인자가 아니어도 강제 반려 허용
|
||||||
|
var hasDeletedApprover = data.approvers.some(approver =>
|
||||||
|
!approver.user || !approver.user.USERID
|
||||||
|
);
|
||||||
|
var isProgressStatus = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
|
||||||
|
document.getElementById("btn_app_force_reject").style.display =
|
||||||
|
(hasDeletedApprover && isProgressStatus && !isCurrentApprover) ? "" : "none";
|
||||||
|
|
||||||
if (data.approvalStatus.code === 'FAILED') {
|
if (data.approvalStatus.code === 'FAILED') {
|
||||||
document.getElementById("btn_app_redeploy").style.display = "";
|
document.getElementById("btn_app_redeploy").style.display = "";
|
||||||
}
|
}
|
||||||
@@ -335,6 +392,13 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="gwActionSection" style="display:none; margin: 12px 0 0 4px; color: #c0392b;">
|
||||||
|
<label style="cursor:pointer;">
|
||||||
|
<input type="checkbox" id="chkGwDelete"/>
|
||||||
|
GW 인증정보 완전 삭제 (미체크 시 차단 처리 — 인증정보 보존, 복구 가능)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="gwActionResult" style="display:none; margin: 12px 0 0 4px; font-weight: bold;"></div>
|
||||||
<div class="popup_button">
|
<div class="popup_button">
|
||||||
<button type="button" class="cssbtn" id="btn_modify" level="W"><i
|
<button type="button" class="cssbtn" id="btn_modify" level="W"><i
|
||||||
class="material-icons">save</i>수정
|
class="material-icons">save</i>수정
|
||||||
@@ -345,6 +409,9 @@
|
|||||||
<button type="button" class="cssbtn" style="display: none;" id="btn_app_reject" level="W"><i
|
<button type="button" class="cssbtn" style="display: none;" id="btn_app_reject" level="W"><i
|
||||||
class="material-icons">close</i>반려
|
class="material-icons">close</i>반려
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="cssbtn" style="display: none;" id="btn_app_force_reject" level="W"><i
|
||||||
|
class="material-icons">block</i>강제 반려
|
||||||
|
</button>
|
||||||
<button type="button" class="cssbtn" style="display: none;" id="btn_app_redeploy" level="W"><i
|
<button type="button" class="cssbtn" style="display: none;" id="btn_app_redeploy" level="W"><i
|
||||||
class="material-icons">close</i>재처리
|
class="material-icons">close</i>재처리
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
.cssbtn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script language="javascript">
|
<script language="javascript">
|
||||||
<%
|
<%
|
||||||
@@ -150,6 +154,55 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btn_user_force_reject").click(function() {
|
||||||
|
if (!confirm("승인자 목록에 삭제된 사용자가 있어 승인 진행이 불가능한 건입니다.\n강제 반려하시겠습니까?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rejectReason = prompt("강제 반려 사유를 입력해 주세요");
|
||||||
|
if (rejectReason != null) {
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: url,
|
||||||
|
data: {cmd: "FORCE_REJECT", id: key, rejectReason: rejectReason},
|
||||||
|
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_user_delete").click(function() {
|
||||||
|
if (!confirm("삭제된 사용자 또는 법인이 포함되어 승인·반려할 수 없는 요청입니다.\n이 승인 요청을 삭제하시겠습니까?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: url,
|
||||||
|
data: {cmd: "DELETE", id: key},
|
||||||
|
success: function(args) {
|
||||||
|
alert("삭제되었습니다.");
|
||||||
|
if (window.dialogArguments && window.dialogArguments.self) {
|
||||||
|
window.dialogArguments.self.location.reload();
|
||||||
|
} else if (window.opener) {
|
||||||
|
window.opener.location.reload();
|
||||||
|
}
|
||||||
|
window.close();
|
||||||
|
},
|
||||||
|
error: function(e) {
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('#btn_user_redeploy').click(function() {
|
$('#btn_user_redeploy').click(function() {
|
||||||
if (confirm("재배포 하시겠습니까?")) {
|
if (confirm("재배포 하시겠습니까?")) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -217,15 +270,41 @@
|
|||||||
|
|
||||||
// Display Portal User details
|
// Display Portal User details
|
||||||
var portalUser = data.portalUser;
|
var portalUser = data.portalUser;
|
||||||
|
var portalOrg = portalUser ? portalUser.portalOrgUIs : null;
|
||||||
|
|
||||||
|
// 삭제된 대상 사용자/법인 판별
|
||||||
|
var userDeleted = !portalUser
|
||||||
|
|| portalUser.userName === '반려된 사용자'
|
||||||
|
|| portalUser.userName === '삭제된 사용자';
|
||||||
|
var orgDeleted = !portalOrg
|
||||||
|
|| portalOrg.orgStatus === 'REMOVED'
|
||||||
|
|| portalOrg.orgName === '삭제된법인';
|
||||||
|
var deletedTarget = userDeleted || orgDeleted;
|
||||||
|
|
||||||
|
if (portalUser) {
|
||||||
$("#loginId").text(portalUser.loginId);
|
$("#loginId").text(portalUser.loginId);
|
||||||
$("#userName").text(portalUser.userName);
|
$("#userName").text(portalUser.userName);
|
||||||
$("#mobileNumber").text(portalUser.mobileNumber);
|
$("#mobileNumber").text(portalUser.mobileNumber);
|
||||||
$("#userStatusDescription").text(portalUser.userStatusDescription);
|
$("#userStatusDescription").text(portalUser.userStatusDescription);
|
||||||
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
|
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
|
||||||
if (data.approvers.some(approver =>
|
} else {
|
||||||
|
$("#loginId, #userName, #mobileNumber, #userStatusDescription, #roleCodeDescription").text("삭제된 사용자");
|
||||||
|
}
|
||||||
|
|
||||||
|
var isCurrentApprover = data.approvers.some(approver =>
|
||||||
approver.approverStatus === 'CURRENT' &&
|
approver.approverStatus === 'CURRENT' &&
|
||||||
approver.user.USERID === loginUser
|
approver.user && approver.user.USERID === loginUser
|
||||||
)) {
|
);
|
||||||
|
|
||||||
|
if (deletedTarget) {
|
||||||
|
// 삭제된 사용자/법인 포함 → 승인·반려 불가. 버튼은 비활성(disable) 표기, 삭제 버튼 노출
|
||||||
|
$("#btn_user_approve, #btn_user_reject").show().prop("disabled", true);
|
||||||
|
document.getElementById("btn_user_force_reject").style.display = "none";
|
||||||
|
document.getElementById("btn_user_delete").style.display = "";
|
||||||
|
} else {
|
||||||
|
$("#btn_user_approve, #btn_user_reject").prop("disabled", false);
|
||||||
|
document.getElementById("btn_user_delete").style.display = "none";
|
||||||
|
if (isCurrentApprover) {
|
||||||
document.getElementById("btn_user_approve").style.display = "";
|
document.getElementById("btn_user_approve").style.display = "";
|
||||||
document.getElementById("btn_user_reject").style.display = "";
|
document.getElementById("btn_user_reject").style.display = "";
|
||||||
} else {
|
} else {
|
||||||
@@ -233,12 +312,21 @@
|
|||||||
document.getElementById("btn_user_reject").style.display = "none";
|
document.getElementById("btn_user_reject").style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 삭제된 승인자(USERID 없음)로 진행 불가한 건: 요청됨/진행중 상태면 승인자가 아니어도 강제 반려 허용
|
||||||
|
var hasDeletedApprover = data.approvers.some(approver =>
|
||||||
|
!approver.user || !approver.user.USERID
|
||||||
|
);
|
||||||
|
var isProgressStatus = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
|
||||||
|
document.getElementById("btn_user_force_reject").style.display =
|
||||||
|
(hasDeletedApprover && isProgressStatus && !isCurrentApprover) ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
if (data.approvalStatus.description === '배포실패'){
|
if (data.approvalStatus.description === '배포실패'){
|
||||||
document.getElementById("btn_user_redeploy").style.display = "";
|
document.getElementById("btn_user_redeploy").style.display = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display Portal Org details
|
// Display Portal Org details
|
||||||
var portalOrg = portalUser.portalOrgUIs;
|
if (portalOrg) {
|
||||||
$("#orgName").text(portalOrg.orgName);
|
$("#orgName").text(portalOrg.orgName);
|
||||||
$("#orgStatusDescription").text(portalOrg.orgStatusDescription);
|
$("#orgStatusDescription").text(portalOrg.orgStatusDescription);
|
||||||
$("#serviceName").text(portalOrg.serviceName);
|
$("#serviceName").text(portalOrg.serviceName);
|
||||||
@@ -253,8 +341,12 @@
|
|||||||
$("#orgSectors").text(portalOrg.orgSectors || ""); // 업태
|
$("#orgSectors").text(portalOrg.orgSectors || ""); // 업태
|
||||||
$("#orgCode").text(portalOrg.orgCode);
|
$("#orgCode").text(portalOrg.orgCode);
|
||||||
$("#ipWhitelist").text(portalOrg.ipWhitelist || "");
|
$("#ipWhitelist").text(portalOrg.ipWhitelist || "");
|
||||||
|
} else {
|
||||||
|
$("#orgName").text("삭제된법인");
|
||||||
|
$("#orgStatusDescription, #serviceName, #corpRegNo, #compRegNo, #scPhoneNumber, #orgPhoneNumber, #ceoName, #orgIndustryType, #orgAddr, #orgSectors, #orgCode, #ipWhitelist").text("");
|
||||||
|
}
|
||||||
|
|
||||||
if (portalOrg.compRegFile) {
|
if (portalOrg && portalOrg.compRegFile) {
|
||||||
$("#fileName").text(portalOrg.fileName);
|
$("#fileName").text(portalOrg.fileName);
|
||||||
if (portalOrg.fileName) {
|
if (portalOrg.fileName) {
|
||||||
$("#fileWrapper").show().off('click').on('click', function() {
|
$("#fileWrapper").show().off('click').on('click', function() {
|
||||||
@@ -294,15 +386,18 @@
|
|||||||
|
|
||||||
// 사용자 정보 업데이트
|
// 사용자 정보 업데이트
|
||||||
var portalUser = data.portalUser;
|
var portalUser = data.portalUser;
|
||||||
|
if (portalUser) {
|
||||||
$("#loginId").text(portalUser.loginId);
|
$("#loginId").text(portalUser.loginId);
|
||||||
$("#userName").text(portalUser.userName);
|
$("#userName").text(portalUser.userName);
|
||||||
$("#mobileNumber").text(portalUser.mobileNumber);
|
$("#mobileNumber").text(portalUser.mobileNumber);
|
||||||
$("#userStatusDescription").text(portalUser.userStatusDescription);
|
$("#userStatusDescription").text(portalUser.userStatusDescription);
|
||||||
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
|
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
|
||||||
|
} else {
|
||||||
|
$("#loginId, #userName, #mobileNumber, #userStatusDescription, #roleCodeDescription").text("삭제된 사용자");
|
||||||
|
}
|
||||||
|
|
||||||
// 법인 정보 업데이트
|
// 법인 정보 업데이트
|
||||||
var portalOrg = portalUser.portalOrgUIs;
|
updateOrgInfo(portalUser ? portalUser.portalOrgUIs : null);
|
||||||
updateOrgInfo(portalOrg);
|
|
||||||
|
|
||||||
// 마스킹 상태에 따라 데이터를 다르게 표시
|
// 마스킹 상태에 따라 데이터를 다르게 표시
|
||||||
var isMasked = $("#btn_unmask").attr("status") === "DETAIL";
|
var isMasked = $("#btn_unmask").attr("status") === "DETAIL";
|
||||||
@@ -344,6 +439,11 @@
|
|||||||
|
|
||||||
// 법인 정보 업데이트 함수
|
// 법인 정보 업데이트 함수
|
||||||
function updateOrgInfo(portalOrg) {
|
function updateOrgInfo(portalOrg) {
|
||||||
|
if (!portalOrg) {
|
||||||
|
$("#orgName").text("삭제된법인");
|
||||||
|
$("#orgStatusDescription, #serviceName, #corpRegNo, #compRegNo, #scPhoneNumber, #orgPhoneNumber, #ceoName, #orgIndustryType, #orgAddr, #orgSectors, #orgCode, #ipWhitelist").text("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
$("#orgName").text(portalOrg.orgName);
|
$("#orgName").text(portalOrg.orgName);
|
||||||
$("#orgStatusDescription").text(portalOrg.orgStatusDescription);
|
$("#orgStatusDescription").text(portalOrg.orgStatusDescription);
|
||||||
$("#serviceName").text(portalOrg.serviceName);
|
$("#serviceName").text(portalOrg.serviceName);
|
||||||
@@ -486,6 +586,8 @@
|
|||||||
<div class="popup_button">
|
<div class="popup_button">
|
||||||
<button type="button" class="cssbtn" style="display: none;" id="btn_user_approve" level="W"><i class="material-icons">check</i>승인</button>
|
<button type="button" class="cssbtn" style="display: none;" id="btn_user_approve" level="W"><i class="material-icons">check</i>승인</button>
|
||||||
<button type="button" class="cssbtn" style="display: none;" id="btn_user_reject" level="W"><i class="material-icons">close</i>반려</button>
|
<button type="button" class="cssbtn" style="display: none;" id="btn_user_reject" level="W"><i class="material-icons">close</i>반려</button>
|
||||||
|
<button type="button" class="cssbtn" style="display: none;" id="btn_user_force_reject" level="W"><i class="material-icons">block</i>강제 반려</button>
|
||||||
|
<button type="button" class="cssbtn" style="display: none;" id="btn_user_delete" level="W"><i class="material-icons">delete</i>삭제</button>
|
||||||
<button type="button" class="cssbtn" style="display: none;" id="btn_user_redeploy" level="W"><i class="material-icons">close</i>재처리</button>
|
<button type="button" class="cssbtn" style="display: none;" id="btn_user_redeploy" level="W"><i class="material-icons">close</i>재처리</button>
|
||||||
<button type="button" class="cssbtn" id="btn_close" level="R"><i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %>
|
<button type="button" class="cssbtn" id="btn_close" level="R"><i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -192,7 +192,6 @@
|
|||||||
<option value="">전체</option>
|
<option value="">전체</option>
|
||||||
<option value="EMAIL">EMAIL</option>
|
<option value="EMAIL">EMAIL</option>
|
||||||
<option value="SMS">SMS</option>
|
<option value="SMS">SMS</option>
|
||||||
<option value="KAKAO">KAKAO</option>
|
|
||||||
<option value="MESSENGER">MESSENGER</option>
|
<option value="MESSENGER">MESSENGER</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,6 +33,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatVisibility(cellvalue, options, rowObject) {
|
||||||
|
if (cellvalue == 'PRIVATE') {
|
||||||
|
return '<span style="color:#c0392b; font-weight:bold;">비공개</span>';
|
||||||
|
} else if (cellvalue == 'ALL') {
|
||||||
|
return '전체공개';
|
||||||
|
} else if (cellvalue == 'ORG') {
|
||||||
|
return '법인공개';
|
||||||
|
}
|
||||||
|
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;">' : '';
|
||||||
@@ -101,13 +112,14 @@
|
|||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
postData : { cmd : 'LIST' },
|
postData : { cmd : 'LIST' },
|
||||||
colNames:['No.', 'id', '제목', '법인명', '상태', '작성자', '등록일', '답변자','답변일'],
|
colNames:['No.', 'id', '제목', '법인명', '상태', '공개범위', '작성자', '등록일', '답변자','답변일'],
|
||||||
colModel:[
|
colModel:[
|
||||||
{ name : 'rowNum' , align:'center', width:50, sortable:false },
|
{ name : 'rowNum' , align:'center', width:50, sortable:false },
|
||||||
{ name : 'id' , align:'center', key:true, hidden:true},
|
{ name : 'id' , align:'center', key:true, hidden:true},
|
||||||
{ name : 'inquirySubject' , align:'left' , width:200 , formatter: formatFile},
|
{ name : 'inquirySubject' , align:'left' , width:200 , formatter: formatFile},
|
||||||
{ name : 'orgName' , align:'center' , width:100 , },
|
{ name : 'orgName' , align:'center' , width:100 , },
|
||||||
{ name : 'inquiryStatus' , align:'center', width:70, formatter: formatInquiryStatus },
|
{ name : 'inquiryStatus' , align:'center', width:70, formatter: formatInquiryStatus },
|
||||||
|
{ name : 'visibility' , align:'center', width:70, formatter: formatVisibility },
|
||||||
{ name : 'inquirer.userName', align:'center', width:70 },
|
{ name : 'inquirer.userName', align:'center', width:70 },
|
||||||
{ name : 'createdDate' , align:'center', width:100, formatter: timeStampFormat },
|
{ name : 'createdDate' , align:'center', width:100, formatter: timeStampFormat },
|
||||||
{ name : 'responderName' , align:'center', width:70 },
|
{ name : 'responderName' , align:'center', width:70 },
|
||||||
|
|||||||
@@ -79,12 +79,46 @@
|
|||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reason-modal-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.reason-modal-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 20px;
|
||||||
|
width: 400px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.reason-modal-box h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.reason-modal-box textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.reason-modal-actions {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script language="javascript">
|
<script language="javascript">
|
||||||
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';
|
var inquiryStatus = 'PENDING';
|
||||||
|
var originalVisibility = 'ORG';
|
||||||
|
var reasonModalCallback = null;
|
||||||
|
|
||||||
function decodeHTMLEntities(text) {
|
function decodeHTMLEntities(text) {
|
||||||
var textArea = document.createElement('textarea');
|
var textArea = document.createElement('textarea');
|
||||||
@@ -124,6 +158,24 @@
|
|||||||
var decodedContent = decodeHTMLEntities(data.responseDetail);
|
var decodedContent = decodeHTMLEntities(data.responseDetail);
|
||||||
$("#responseDetail").summernote('code', decodedContent || '');
|
$("#responseDetail").summernote('code', decodedContent || '');
|
||||||
|
|
||||||
|
// 공개범위 / 조회수
|
||||||
|
$('#visibilitySelect').val(data.visibility || 'ORG');
|
||||||
|
originalVisibility = data.visibility || 'ORG';
|
||||||
|
$('#viewCount').text(data.viewCount || 0);
|
||||||
|
|
||||||
|
// 조회수 증가 (sessionStorage dedup — 새로고침 시 중복 증가 억제)
|
||||||
|
var viewedKey = 'inquiry_viewed_' + key;
|
||||||
|
if (!sessionStorage.getItem(viewedKey)) {
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: url,
|
||||||
|
data: {cmd: 'INCREMENT_VIEW', id: key},
|
||||||
|
success: function () {
|
||||||
|
sessionStorage.setItem(viewedKey, 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 댓글 표시
|
// 댓글 표시
|
||||||
listComment(key);
|
listComment(key);
|
||||||
|
|
||||||
@@ -149,8 +201,17 @@
|
|||||||
if (!data || data.length === 0) return;
|
if (!data || data.length === 0) return;
|
||||||
$.each(data, function (i, comment) {
|
$.each(data, function (i, comment) {
|
||||||
var date = comment.createdDate ? timeStampFormat(comment.createdDate) : '';
|
var date = comment.createdDate ? timeStampFormat(comment.createdDate) : '';
|
||||||
|
var isPrivate = comment.visibility === 'PRIVATE';
|
||||||
var row = $('<div>').addClass('info-row');
|
var row = $('<div>').addClass('info-row');
|
||||||
$('<div>').addClass('info-label').css('color', '#555').text(comment.writerName || '').appendTo(row);
|
if (isPrivate) {
|
||||||
|
// 비공개 댓글은 색상으로 구별
|
||||||
|
row.css({'background-color': '#FFF5F5', 'border-radius': '4px', 'padding': '4px'});
|
||||||
|
}
|
||||||
|
var labelDiv = $('<div>').addClass('info-label').css('color', '#555').text(comment.writerName || '');
|
||||||
|
if (isPrivate) {
|
||||||
|
$('<span>').text(' 비공개').css({'color': '#c0392b', 'font-size': '0.85em', 'font-weight': 'bold'}).appendTo(labelDiv);
|
||||||
|
}
|
||||||
|
labelDiv.appendTo(row);
|
||||||
var content = $('<div>').addClass('info-content');
|
var content = $('<div>').addClass('info-content');
|
||||||
$('<div>').css('white-space', 'pre-wrap').text(comment.commentDetail).appendTo(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'});
|
var meta = $('<div>').css({'font-size': '0.9em', 'color': '#999', 'margin-top': '2px'});
|
||||||
@@ -161,6 +222,19 @@
|
|||||||
})(comment.id, comment.inquiryId))
|
})(comment.id, comment.inquiryId))
|
||||||
.appendTo(meta);
|
.appendTo(meta);
|
||||||
meta.appendTo(content);
|
meta.appendTo(content);
|
||||||
|
|
||||||
|
// 댓글 공개범위 변경 컨트롤 (사유 필수)
|
||||||
|
var visCtrl = $('<div>').css({'margin-top': '4px', 'font-size': '0.85em'});
|
||||||
|
var sel = $('<select>').append('<option value="ALL">공개</option>').append('<option value="PRIVATE">비공개</option>');
|
||||||
|
sel.val(comment.visibility || 'ALL');
|
||||||
|
var reasonInput = $('<input>').attr({'type': 'text', 'placeholder': '변경 사유'}).css({'margin-left': '4px'});
|
||||||
|
var applyBtn = $('<button>').attr('type', 'button').addClass('cssbtn smallBtn2').css({'margin-left': '4px', 'font-size': '0.85em', 'padding': '1px 6px'})
|
||||||
|
.text('공개범위변경').on('click', (function(id, inquiryId, selEl, reasonEl) {
|
||||||
|
return function() { changeCommentVisibility(id, inquiryId, selEl.val(), reasonEl.val()); };
|
||||||
|
})(comment.id, comment.inquiryId, sel, reasonInput));
|
||||||
|
visCtrl.append(sel).append(reasonInput).append(applyBtn);
|
||||||
|
visCtrl.appendTo(content);
|
||||||
|
|
||||||
content.appendTo(row);
|
content.appendTo(row);
|
||||||
row.appendTo($commentList);
|
row.appendTo($commentList);
|
||||||
});
|
});
|
||||||
@@ -171,6 +245,84 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openReasonModal(callback) {
|
||||||
|
reasonModalCallback = callback;
|
||||||
|
$('#reasonModalInput').val('');
|
||||||
|
$('#reasonModal').show();
|
||||||
|
$('#reasonModalInput').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeReasonModal() {
|
||||||
|
$('#reasonModal').hide();
|
||||||
|
reasonModalCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 답변(summernote) 내용이 실제로 입력됐는지 확인
|
||||||
|
function hasAnswerContent() {
|
||||||
|
var v = $('#responseDetail').summernote('code');
|
||||||
|
var text = $('<div>').html(v || '').text().replace(/ /g, ' ').trim();
|
||||||
|
return text.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveInquiryAnswer(returnUrl) {
|
||||||
|
var postData = $('#ajaxForm').serializeArray();
|
||||||
|
postData.push({name: "cmd", value: "INSERT"});
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: url,
|
||||||
|
data: postData,
|
||||||
|
success: function (json) {
|
||||||
|
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||||
|
goNav(returnUrl);
|
||||||
|
},
|
||||||
|
error: function (e) {
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 게시물 공개범위 변경(사유 필수). 성공 후 답변 내용이 있으면 이어서 저장.
|
||||||
|
function changeInquiryVisibility(newVis, reason, returnUrl, thenSaveAnswer) {
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: url,
|
||||||
|
data: {cmd: 'VISIBILITY', serviceType: 'APIGW', targetType: 'INQUIRY', id: $('#id').val(),
|
||||||
|
visibility: newVis, auditReason: reason},
|
||||||
|
success: function () {
|
||||||
|
originalVisibility = newVis;
|
||||||
|
if (thenSaveAnswer) {
|
||||||
|
saveInquiryAnswer(returnUrl);
|
||||||
|
} else {
|
||||||
|
alert('변경되었습니다.');
|
||||||
|
goNav(returnUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (e) {
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeCommentVisibility(commentId, inquiryId, visibility, reason) {
|
||||||
|
if (!reason || !reason.trim()) {
|
||||||
|
alert('공개범위 변경 사유를 입력하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm('댓글 공개범위를 변경하시겠습니까?')) return;
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: url,
|
||||||
|
data: {cmd: 'VISIBILITY', serviceType: 'APIGW', targetType: 'COMMENT', id: commentId, visibility: visibility, auditReason: reason},
|
||||||
|
success: function () {
|
||||||
|
alert('변경되었습니다.');
|
||||||
|
listComment(inquiryId);
|
||||||
|
},
|
||||||
|
error: function (e) {
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function deleteComment(commentId, inquiryId) {
|
function deleteComment(commentId, inquiryId) {
|
||||||
if (inquiryStatus == 'CLOSED') {
|
if (inquiryStatus == 'CLOSED') {
|
||||||
alert('답변이 종료되어 삭제할 수 없습니다');
|
alert('답변이 종료되어 삭제할 수 없습니다');
|
||||||
@@ -210,24 +362,38 @@
|
|||||||
alert('답변이 종료되어 수정할 수 없습니다');
|
alert('답변이 종료되어 수정할 수 없습니다');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var newVis = $('#visibilitySelect').val();
|
||||||
|
var visChanged = (newVis !== originalVisibility);
|
||||||
|
|
||||||
|
if (visChanged) {
|
||||||
|
// 공개범위 변경 감지 → 사유 입력 모달 (답변 내용이 있으면 함께 저장)
|
||||||
|
var saveAnswerToo = hasAnswerContent();
|
||||||
|
openReasonModal(function (reason) {
|
||||||
|
changeInquiryVisibility(newVis, reason, returnUrl, saveAnswerToo);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공개범위 변경 없음 → 기존 답변 저장
|
||||||
if (!checkRequired("ajaxForm")) return;
|
if (!checkRequired("ajaxForm")) return;
|
||||||
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
|
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
|
||||||
|
saveInquiryAnswer(returnUrl);
|
||||||
var postData = $('#ajaxForm').serializeArray();
|
|
||||||
postData.push({name: "cmd", value: "INSERT"});
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: "POST",
|
|
||||||
url: url,
|
|
||||||
data: postData,
|
|
||||||
success: function (json) {
|
|
||||||
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
|
||||||
goNav(returnUrl);
|
|
||||||
},
|
|
||||||
error: function (e) {
|
|
||||||
alert(e.responseText);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#reasonModalOk").click(function () {
|
||||||
|
var reason = ($('#reasonModalInput').val() || '').trim();
|
||||||
|
if (!reason) {
|
||||||
|
alert('변경 사유를 입력하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cb = reasonModalCallback;
|
||||||
|
closeReasonModal();
|
||||||
|
if (cb) cb(reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#reasonModalCancel").click(function () {
|
||||||
|
closeReasonModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btn_comment").click(function () {
|
$("#btn_comment").click(function () {
|
||||||
@@ -310,6 +476,21 @@
|
|||||||
<div class="info-label">작성일</div>
|
<div class="info-label">작성일</div>
|
||||||
<div id="createdDate" class="info-content"></div>
|
<div id="createdDate" class="info-content"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-label">조회수</div>
|
||||||
|
<div id="viewCount" class="info-content">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-label">공개범위</div>
|
||||||
|
<div class="info-content" style="display:flex; gap:8px; align-items:center;">
|
||||||
|
<select id="visibilitySelect">
|
||||||
|
<option value="ALL">전체공개</option>
|
||||||
|
<option value="ORG">법인공개</option>
|
||||||
|
<option value="PRIVATE">비공개</option>
|
||||||
|
</select>
|
||||||
|
<span style="font-size:0.85em; color:#999;">변경 후 상단 [수정] 버튼을 누르면 사유 입력창이 표시됩니다.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="info-row" id="attachFileRow" style="display: none;">
|
<div class="info-row" id="attachFileRow" style="display: none;">
|
||||||
<div class="info-label">첨부파일</div>
|
<div class="info-label">첨부파일</div>
|
||||||
<div id="attachFiles" class="info-content"></div>
|
<div id="attachFiles" class="info-content"></div>
|
||||||
@@ -358,5 +539,17 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 공개범위 변경 사유 입력 모달 -->
|
||||||
|
<div id="reasonModal" class="reason-modal-overlay">
|
||||||
|
<div class="reason-modal-box">
|
||||||
|
<h3>공개범위 변경 사유</h3>
|
||||||
|
<textarea id="reasonModalInput" placeholder="변경 사유를 입력하세요."></textarea>
|
||||||
|
<div class="reason-modal-actions">
|
||||||
|
<button type="button" class="cssbtn" id="reasonModalOk">확인</button>
|
||||||
|
<button type="button" class="cssbtn" id="reasonModalCancel">취소</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,550 @@
|
|||||||
|
<%@ 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" %>
|
||||||
|
<%
|
||||||
|
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"/>
|
||||||
|
<jsp:include page="/jsp/common/include/portal.jsp"/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ── 스윔레인 보드 ── */
|
||||||
|
.menu-board { display: flex; gap: 12px; align-items: flex-start; }
|
||||||
|
.menu-lane-strip { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 10px; flex: 1 1 auto; }
|
||||||
|
.menu-lane { background: #f1f3f5; border-radius: 10px; min-width: 230px; max-width: 230px; flex: 0 0 230px; }
|
||||||
|
.menu-lane.unplaced { background: #fff4e6; border: 1px dashed #fd7e14; }
|
||||||
|
.lane-header { display: flex; align-items: center; gap: 6px; padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #dee2e6; cursor: grab; }
|
||||||
|
.menu-lane.unplaced .lane-header { cursor: default; }
|
||||||
|
.lane-title { font-weight: 700; font-size: 14px; flex: 1 1 auto; overflow: hidden;
|
||||||
|
text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.lane-body { list-style: none; margin: 0; padding: 8px; min-height: 60px; }
|
||||||
|
.menu-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px;
|
||||||
|
padding: 8px 10px; margin-bottom: 8px; cursor: grab;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.06); }
|
||||||
|
.menu-card.hidden-item, .menu-lane.hidden-item > .lane-header { opacity: 0.45; }
|
||||||
|
.menu-card .card-name { font-size: 13px; font-weight: 600; }
|
||||||
|
.menu-card .card-id { font-size: 11px; color: #868e96; }
|
||||||
|
.menu-card .card-path { font-size: 11px; color: #495057; word-break: break-all; }
|
||||||
|
.card-actions, .lane-actions { display: flex; gap: 4px; }
|
||||||
|
.icon-btn { border: none; background: transparent; padding: 1px 3px; cursor: pointer;
|
||||||
|
font-size: 14px; color: #495057; }
|
||||||
|
.icon-btn:hover { color: #0d6efd; }
|
||||||
|
.icon-btn.danger:hover { color: #dc3545; }
|
||||||
|
.badge-src { font-size: 10px; }
|
||||||
|
.role-check-group { display: flex; flex-wrap: wrap; gap: 4px 12px; }
|
||||||
|
.role-check-group .form-check { min-width: 45%; }
|
||||||
|
.dflt-hint { font-size: 11px; color: #868e96; }
|
||||||
|
.board-toolbar { display: flex; gap: 6px; margin-bottom: 10px; align-items: center; }
|
||||||
|
.board-toolbar .spacer { flex: 1 1 auto; }
|
||||||
|
.sortable-placeholder { border: 2px dashed #adb5bd; border-radius: 8px; height: 48px;
|
||||||
|
margin-bottom: 8px; background: #e9ecef; }
|
||||||
|
.lane-placeholder { border: 2px dashed #adb5bd; border-radius: 10px; min-width: 230px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script language="javascript">
|
||||||
|
// AuthorizeInterceptor 는 serviceType 을 "요청 파라미터"에서 읽으므로
|
||||||
|
// 모든 .json 호출 URL 에 serviceType 을 부착한다 (누락 시 권한 조회 실패 → Access denied)
|
||||||
|
var url = urlAddServiceType('<c:url value="/onl/apim/portalmenu/portalMenuMan.json" />');
|
||||||
|
|
||||||
|
// 화면 상태 (저장 버튼 클릭 시에만 서버 반영 — 드래그/토글은 클라이언트 상태만 변경)
|
||||||
|
var state = { itemsById: {}, lanes: [], unplaced: [], roles: [] };
|
||||||
|
var dirty = false;
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s == null ? '' : s)
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAll(afterFn) {
|
||||||
|
$.post(url, { cmd: 'LIST_ALL' }, function (data) {
|
||||||
|
buildState(data);
|
||||||
|
render();
|
||||||
|
dirty = false;
|
||||||
|
if (afterFn) afterFn();
|
||||||
|
}, 'json').fail(ajaxFail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildState(data) {
|
||||||
|
state.itemsById = {};
|
||||||
|
state.roles = data.roles || [];
|
||||||
|
(data.items || []).forEach(function (it) { state.itemsById[it.menuId] = it; });
|
||||||
|
|
||||||
|
var placedTop = [], childrenByParent = {}, unplaced = [];
|
||||||
|
(data.items || []).forEach(function (it) {
|
||||||
|
if (!it.placed) { unplaced.push(it.menuId); return; }
|
||||||
|
if (!it.parentId) {
|
||||||
|
placedTop.push(it);
|
||||||
|
} else {
|
||||||
|
(childrenByParent[it.parentId] = childrenByParent[it.parentId] || []).push(it);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
placedTop.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); });
|
||||||
|
|
||||||
|
state.lanes = placedTop.map(function (top) {
|
||||||
|
var kids = (childrenByParent[top.menuId] || [])
|
||||||
|
.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); })
|
||||||
|
.map(function (c) { return { menuId: c.menuId, visibleYn: c.visibleYn || 'Y' }; });
|
||||||
|
delete childrenByParent[top.menuId];
|
||||||
|
return { menuId: top.menuId, visibleYn: top.visibleYn || 'Y', children: kids };
|
||||||
|
});
|
||||||
|
// 부모가 미배치/부재인 고아 배치는 미배치로 강등
|
||||||
|
Object.keys(childrenByParent).forEach(function (pid) {
|
||||||
|
childrenByParent[pid].forEach(function (c) { unplaced.push(c.menuId); });
|
||||||
|
});
|
||||||
|
state.unplaced = unplaced;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 렌더 ──
|
||||||
|
function cardHtml(menuId, visibleYn, inLane) {
|
||||||
|
var it = state.itemsById[menuId];
|
||||||
|
if (!it) return '';
|
||||||
|
var hidden = visibleYn === 'N';
|
||||||
|
var srcBadge = it.sourceType === 'PORTAL'
|
||||||
|
? '<span class="badge text-bg-secondary badge-src">기본</span>'
|
||||||
|
: '<span class="badge text-bg-warning badge-src">커스텀</span>';
|
||||||
|
var grpBadge = it.groupYn === 'Y' ? ' <span class="badge text-bg-info badge-src">그룹</span>' : '';
|
||||||
|
var placeBtn = (!inLane && it.groupYn === 'Y')
|
||||||
|
? '<button type="button" class="icon-btn" title="최상위 레인으로 배치" onclick="placeAsLane(\'' + menuId + '\')"><i class="bi bi-box-arrow-in-up"></i></button>'
|
||||||
|
: '';
|
||||||
|
var eyeBtn = inLane
|
||||||
|
? '<button type="button" class="icon-btn" title="노출/숨김" onclick="toggleVisible(\'' + menuId + '\')"><i class="bi ' + (hidden ? 'bi-eye-slash' : 'bi-eye') + '"></i></button>'
|
||||||
|
: '';
|
||||||
|
return '<li class="menu-card' + (hidden ? ' hidden-item' : '') + '" data-menu-id="' + escapeHtml(menuId) + '">'
|
||||||
|
+ '<div class="d-flex align-items-start">'
|
||||||
|
+ '<div class="flex-grow-1">'
|
||||||
|
+ '<div class="card-name">' + escapeHtml(it.menuName) + ' ' + srcBadge + grpBadge + '</div>'
|
||||||
|
+ '<div class="card-id">' + escapeHtml(it.menuId) + '</div>'
|
||||||
|
+ (it.menuPath ? '<div class="card-path">' + escapeHtml(it.menuPath) + '</div>' : '')
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="card-actions">'
|
||||||
|
+ placeBtn + eyeBtn
|
||||||
|
+ '<button type="button" class="icon-btn" title="수정" onclick="openModal(\'' + menuId + '\')"><i class="bi bi-pencil"></i></button>'
|
||||||
|
+ '<button type="button" class="icon-btn danger" level="W" title="' + (it.sourceType === 'PORTAL' ? '미배치로 이동' : '삭제') + '" onclick="minusItem(\'' + menuId + '\')"><i class="bi bi-dash-circle"></i></button>'
|
||||||
|
+ '</div></div></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function laneHtml(lane) {
|
||||||
|
var it = state.itemsById[lane.menuId];
|
||||||
|
if (!it) return '';
|
||||||
|
var hidden = lane.visibleYn === 'N';
|
||||||
|
var sectionBadge = it.menuSection === 'MYPAGE'
|
||||||
|
? '<span class="badge text-bg-primary badge-src">마이페이지</span>'
|
||||||
|
: '<span class="badge text-bg-success badge-src">GNB</span>';
|
||||||
|
var srcBadge = it.sourceType === 'PORTAL' ? '' : ' <span class="badge text-bg-warning badge-src">커스텀</span>';
|
||||||
|
var cards = lane.children.map(function (c) { return cardHtml(c.menuId, c.visibleYn, true); }).join('');
|
||||||
|
return '<div class="menu-lane' + (hidden ? ' hidden-item' : '') + '" data-menu-id="' + escapeHtml(lane.menuId) + '">'
|
||||||
|
+ '<div class="lane-header lane-drag-handle">'
|
||||||
|
+ '<span class="lane-title">' + escapeHtml(it.menuName) + '</span>'
|
||||||
|
+ sectionBadge + srcBadge
|
||||||
|
+ '<div class="lane-actions">'
|
||||||
|
+ '<button type="button" class="icon-btn" title="노출/숨김" onclick="toggleVisible(\'' + lane.menuId + '\')"><i class="bi ' + (hidden ? 'bi-eye-slash' : 'bi-eye') + '"></i></button>'
|
||||||
|
+ '<button type="button" class="icon-btn" title="수정" onclick="openModal(\'' + lane.menuId + '\')"><i class="bi bi-pencil"></i></button>'
|
||||||
|
+ '<button type="button" class="icon-btn danger" level="W" title="레인 미배치" onclick="unplaceLane(\'' + lane.menuId + '\')"><i class="bi bi-dash-circle"></i></button>'
|
||||||
|
+ '</div></div>'
|
||||||
|
+ '<ul class="lane-body" data-lane-id="' + escapeHtml(lane.menuId) + '">' + cards + '</ul>'
|
||||||
|
+ '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
var strip = state.lanes.map(laneHtml).join('');
|
||||||
|
$('#laneStrip').html(strip);
|
||||||
|
$('#unplacedBody').html(state.unplaced.map(function (id) { return cardHtml(id, 'Y', false); }).join(''));
|
||||||
|
initSortables();
|
||||||
|
buttonControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSortables() {
|
||||||
|
$('#laneStrip').sortable({
|
||||||
|
items: '.menu-lane',
|
||||||
|
handle: '.lane-drag-handle',
|
||||||
|
placeholder: 'lane-placeholder',
|
||||||
|
tolerance: 'pointer',
|
||||||
|
update: function () { syncFromDom(); }
|
||||||
|
});
|
||||||
|
$('.lane-body, #unplacedBody').sortable({
|
||||||
|
connectWith: '.lane-body, #unplacedBody',
|
||||||
|
items: '.menu-card',
|
||||||
|
placeholder: 'sortable-placeholder',
|
||||||
|
tolerance: 'pointer',
|
||||||
|
stop: function () { syncFromDom(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 드래그 종료 후 DOM 순서 → 상태 동기화 (visible 값은 기존 상태 유지)
|
||||||
|
function syncFromDom() {
|
||||||
|
var visibleMap = {};
|
||||||
|
state.lanes.forEach(function (l) {
|
||||||
|
visibleMap[l.menuId] = l.visibleYn;
|
||||||
|
l.children.forEach(function (c) { visibleMap[c.menuId] = c.visibleYn; });
|
||||||
|
});
|
||||||
|
|
||||||
|
var lanes = [];
|
||||||
|
$('#laneStrip .menu-lane').each(function () {
|
||||||
|
var laneId = $(this).data('menu-id');
|
||||||
|
var children = [];
|
||||||
|
$(this).find('.lane-body .menu-card').each(function () {
|
||||||
|
var id = $(this).data('menu-id');
|
||||||
|
children.push({ menuId: id, visibleYn: visibleMap[id] || 'Y' });
|
||||||
|
});
|
||||||
|
lanes.push({ menuId: laneId, visibleYn: visibleMap[laneId] || 'Y', children: children });
|
||||||
|
});
|
||||||
|
var unplaced = [];
|
||||||
|
$('#unplacedBody .menu-card').each(function () { unplaced.push($(this).data('menu-id')); });
|
||||||
|
|
||||||
|
state.lanes = lanes;
|
||||||
|
state.unplaced = unplaced;
|
||||||
|
dirty = true;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 카드/레인 조작 ──
|
||||||
|
function toggleVisible(menuId) {
|
||||||
|
state.lanes.forEach(function (l) {
|
||||||
|
if (l.menuId === menuId) l.visibleYn = (l.visibleYn === 'N' ? 'Y' : 'N');
|
||||||
|
l.children.forEach(function (c) {
|
||||||
|
if (c.menuId === menuId) c.visibleYn = (c.visibleYn === 'N' ? 'Y' : 'N');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
dirty = true;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeAsLane(menuId) {
|
||||||
|
state.unplaced = state.unplaced.filter(function (id) { return id !== menuId; });
|
||||||
|
state.lanes.push({ menuId: menuId, visibleYn: 'Y', children: [] });
|
||||||
|
dirty = true;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function minusItem(menuId) {
|
||||||
|
var it = state.itemsById[menuId];
|
||||||
|
if (!it) return;
|
||||||
|
if (it.sourceType === 'PORTAL') {
|
||||||
|
removeFromBoard(menuId);
|
||||||
|
if (state.unplaced.indexOf(menuId) < 0) state.unplaced.push(menuId);
|
||||||
|
dirty = true;
|
||||||
|
render();
|
||||||
|
} else {
|
||||||
|
if (!confirm('커스텀 항목은 미배치 시 삭제됩니다.\n[' + it.menuName + '] 을(를) 삭제하시겠습니까?')) return;
|
||||||
|
$.post(url, { cmd: 'DELETE', menuId: menuId }, function () {
|
||||||
|
removeFromBoard(menuId);
|
||||||
|
state.unplaced = state.unplaced.filter(function (id) { return id !== menuId; });
|
||||||
|
delete state.itemsById[menuId];
|
||||||
|
dirty = true;
|
||||||
|
render();
|
||||||
|
}, 'json').fail(ajaxFail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unplaceLane(laneId) {
|
||||||
|
var lane = null;
|
||||||
|
state.lanes = state.lanes.filter(function (l) {
|
||||||
|
if (l.menuId === laneId) { lane = l; return false; }
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (!lane) return;
|
||||||
|
lane.children.forEach(function (c) {
|
||||||
|
if (state.unplaced.indexOf(c.menuId) < 0) state.unplaced.push(c.menuId);
|
||||||
|
});
|
||||||
|
var it = state.itemsById[laneId];
|
||||||
|
if (it && it.sourceType !== 'PORTAL') {
|
||||||
|
if (confirm('커스텀 항목은 미배치 시 삭제됩니다.\n[' + it.menuName + '] 항목을 삭제하시겠습니까?')) {
|
||||||
|
$.post(url, { cmd: 'DELETE', menuId: laneId }, function () {
|
||||||
|
delete state.itemsById[laneId];
|
||||||
|
render();
|
||||||
|
}, 'json').fail(ajaxFail);
|
||||||
|
} else {
|
||||||
|
state.unplaced.push(laneId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (state.unplaced.indexOf(laneId) < 0) state.unplaced.push(laneId);
|
||||||
|
}
|
||||||
|
dirty = true;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromBoard(menuId) {
|
||||||
|
state.lanes = state.lanes.filter(function (l) { return l.menuId !== menuId; });
|
||||||
|
state.lanes.forEach(function (l) {
|
||||||
|
l.children = l.children.filter(function (c) { return c.menuId !== menuId; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 저장 / 초기화 / 캐시 Reload ──
|
||||||
|
function savePlacements() {
|
||||||
|
// 미배치 상태의 커스텀 항목은 저장 시 삭제 처리 (스펙: 커스텀은 미배치=삭제)
|
||||||
|
var unplacedAdmin = state.unplaced.filter(function (id) {
|
||||||
|
var it = state.itemsById[id];
|
||||||
|
return it && it.sourceType !== 'PORTAL';
|
||||||
|
});
|
||||||
|
if (unplacedAdmin.length > 0) {
|
||||||
|
var names = unplacedAdmin.map(function (id) { return state.itemsById[id].menuName; }).join(', ');
|
||||||
|
if (!confirm('미배치된 커스텀 항목은 저장 시 삭제됩니다: ' + names + '\n계속하시겠습니까?')) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = [];
|
||||||
|
state.lanes.forEach(function (l, i) {
|
||||||
|
rows.push({ menuId: l.menuId, parentId: null, sortOrder: (i + 1) * 10, visibleYn: l.visibleYn });
|
||||||
|
l.children.forEach(function (c, j) {
|
||||||
|
rows.push({ menuId: c.menuId, parentId: l.menuId, sortOrder: (j + 1) * 10, visibleYn: c.visibleYn });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var deleteCalls = unplacedAdmin.map(function (id) {
|
||||||
|
return $.post(url, { cmd: 'DELETE', menuId: id });
|
||||||
|
});
|
||||||
|
$.when.apply($, deleteCalls).always(function () {
|
||||||
|
$.ajax({
|
||||||
|
url: url + '&cmd=TRANSACTION_PLACEMENT',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json; charset=utf-8',
|
||||||
|
data: JSON.stringify(rows)
|
||||||
|
}).done(function () {
|
||||||
|
dirty = false;
|
||||||
|
loadAll(function () {
|
||||||
|
if (confirm('배치가 저장되었습니다.\n포탈 메뉴 캐시를 즉시 반영(Reload)하시겠습니까?\n(미반영 시 캐시 TTL 후 자동 반영)')) {
|
||||||
|
reloadPortalCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).fail(ajaxFail);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeMenus() {
|
||||||
|
if (!confirm('메뉴 초기화 시 포탈 기본(menu.yml) 값으로 복원되고\n커스텀 항목은 모두 삭제됩니다. 계속하시겠습니까?')) return;
|
||||||
|
$.post(url, { cmd: 'INITIALIZE' }, function () {
|
||||||
|
loadAll(function () { alert('메뉴가 초기화되었습니다.'); });
|
||||||
|
}, 'json').fail(ajaxFail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadPortalCache() {
|
||||||
|
$.post(url, { cmd: 'TRANSACTION_RELOAD' }, function (result) {
|
||||||
|
if (result && result.success) {
|
||||||
|
alert('포탈 메뉴 캐시 리로드 완료 (항목 ' + result.itemCount + '건, ' + result.reloadedAt + ')');
|
||||||
|
} else {
|
||||||
|
alert('캐시 리로드 실패: ' + (result ? result.message : '알 수 없는 오류'));
|
||||||
|
}
|
||||||
|
}, 'json').fail(ajaxFail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ajaxFail(xhr) {
|
||||||
|
var msg = '처리 중 오류가 발생했습니다.';
|
||||||
|
try {
|
||||||
|
var body = JSON.parse(xhr.responseText);
|
||||||
|
if (body.errorMsg) msg = body.errorMsg;
|
||||||
|
else if (body.message) msg = body.message;
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
alert(msg + ' (HTTP ' + xhr.status + ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 추가/수정 모달 ──
|
||||||
|
var menuModal;
|
||||||
|
|
||||||
|
function roleChecksHtml(namePrefix, checkedCsv) {
|
||||||
|
var checked = (checkedCsv || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
||||||
|
var html = '';
|
||||||
|
var all = [{ roleCode: 'AUTHENTICATED', roleName: '로그인 사용자', roleType: '-' }].concat(state.roles);
|
||||||
|
all.forEach(function (r) {
|
||||||
|
var id = namePrefix + '_' + r.roleCode;
|
||||||
|
html += '<div class="form-check form-check-sm">'
|
||||||
|
+ '<input class="form-check-input" type="checkbox" name="' + namePrefix + '" id="' + id + '" value="' + escapeHtml(r.roleCode) + '"'
|
||||||
|
+ (checked.indexOf(r.roleCode) >= 0 ? ' checked' : '') + '>'
|
||||||
|
+ '<label class="form-check-label" for="' + id + '" style="font-size:12px;">'
|
||||||
|
+ escapeHtml(r.roleName) + ' <span class="text-muted">(' + escapeHtml(r.roleCode) + ')</span></label>'
|
||||||
|
+ '</div>';
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(menuId) {
|
||||||
|
var isNew = !menuId;
|
||||||
|
var it = isNew ? { sourceType: 'ADMIN', groupYn: 'N', menuSection: 'GNB', newWindowYn: 'N' }
|
||||||
|
: state.itemsById[menuId];
|
||||||
|
if (!it) return;
|
||||||
|
var isPortal = it.sourceType === 'PORTAL';
|
||||||
|
|
||||||
|
$('#modalTitle').text(isNew ? '메뉴 추가' : '메뉴 수정' + (isPortal ? ' (기본 항목)' : ''));
|
||||||
|
$('#fMenuId').val(it.menuId || '').prop('readonly', !isNew);
|
||||||
|
$('#fMenuName').val(it.menuName || '');
|
||||||
|
$('#fMenuPath').val(it.menuPath || '');
|
||||||
|
$('#fIconClass').val(it.iconClass || '');
|
||||||
|
$('#fMenuSection').val(it.menuSection || 'GNB').prop('disabled', isPortal);
|
||||||
|
$('#fGroupYn').prop('checked', it.groupYn === 'Y').prop('disabled', isPortal);
|
||||||
|
$('#fNewWindowYn').prop('checked', it.newWindowYn === 'Y');
|
||||||
|
$('#exposeRoleChecks').html(roleChecksHtml('exposeRole', it.exposeRoles));
|
||||||
|
$('#accessRoleChecks').html(roleChecksHtml('accessRole', it.accessRoles));
|
||||||
|
|
||||||
|
// 기본 항목: 기본값(DFLT_*) 병기
|
||||||
|
if (isPortal) {
|
||||||
|
$('#hintMenuName').text('기본값: ' + (it.dfltMenuName || '-'));
|
||||||
|
$('#hintMenuPath').text('기본값: ' + (it.dfltMenuPath || '-'));
|
||||||
|
$('#hintExposeRoles').text('기본값: ' + (it.dfltExposeRoles || '전체 노출'));
|
||||||
|
$('#hintAccessRoles').text('기본값: ' + (it.dfltAccessRoles || '제한 없음'));
|
||||||
|
$('.dflt-hint').show();
|
||||||
|
} else {
|
||||||
|
$('.dflt-hint').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#modalMode').val(isNew ? 'INSERT' : 'UPDATE');
|
||||||
|
menuModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitModal() {
|
||||||
|
var cmd = $('#modalMode').val();
|
||||||
|
var data = {
|
||||||
|
cmd: cmd,
|
||||||
|
menuId: $('#fMenuId').val().trim(),
|
||||||
|
menuName: $('#fMenuName').val().trim(),
|
||||||
|
menuPath: $('#fMenuPath').val().trim(),
|
||||||
|
iconClass: $('#fIconClass').val().trim(),
|
||||||
|
menuSection: $('#fMenuSection').val(),
|
||||||
|
groupYn: $('#fGroupYn').is(':checked') ? 'Y' : 'N',
|
||||||
|
newWindowYn: $('#fNewWindowYn').is(':checked') ? 'Y' : 'N',
|
||||||
|
exposeRoles: $('input[name=exposeRole]:checked').map(function () { return this.value; }).get().join(','),
|
||||||
|
accessRoles: $('input[name=accessRole]:checked').map(function () { return this.value; }).get().join(',')
|
||||||
|
};
|
||||||
|
if (!data.menuId) { alert('메뉴 ID 를 입력하세요.'); return; }
|
||||||
|
if (cmd === 'INSERT' && !/^[a-z0-9-]+$/.test(data.menuId)) {
|
||||||
|
alert('메뉴 ID 는 kebab-case(소문자/숫자/하이픈)만 허용합니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data.menuName) { alert('노출명을 입력하세요.'); return; }
|
||||||
|
|
||||||
|
$.post(url, data, function () {
|
||||||
|
menuModal.hide();
|
||||||
|
loadAll();
|
||||||
|
}, 'json').fail(ajaxFail);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
menuModal = new bootstrap.Modal(document.getElementById('menuModal'));
|
||||||
|
loadAll();
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', function (e) {
|
||||||
|
if (dirty) { e.preventDefault(); e.returnValue = ''; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</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="title">포탈 메뉴 관리</div>
|
||||||
|
|
||||||
|
<div class="board-toolbar">
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" level="W" onclick="openModal()">
|
||||||
|
<i class="bi bi-plus-lg"></i> 메뉴 추가</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" level="W" onclick="initializeMenus()">
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i> 메뉴 초기화</button>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" level="W" onclick="reloadPortalCache()">
|
||||||
|
<i class="bi bi-arrow-repeat"></i> 캐시 Reload</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-success" level="W" onclick="savePlacements()">
|
||||||
|
<i class="bi bi-save"></i> 저장</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background:#e7f1ff; border:1px solid #0d6efd33; padding:8px 15px; margin-bottom:10px; border-radius:6px; font-size:12px; color:#31507a;">
|
||||||
|
드래그로 위치를 조절한 뒤 <strong>저장</strong> 버튼으로 일괄 반영합니다.
|
||||||
|
기본 항목([기본] 배지)은 [-] 시 <strong>미배치</strong>로만 이동하고, 커스텀 항목은 미배치 시 <strong>삭제</strong>됩니다.
|
||||||
|
저장 후 <strong>캐시 Reload</strong> 를 실행해야 포탈에 즉시 반영됩니다(미실행 시 TTL 경과 후 반영).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-board">
|
||||||
|
<div class="menu-lane-strip" id="laneStrip"><!-- 렌더링 --></div>
|
||||||
|
<div class="menu-lane unplaced">
|
||||||
|
<div class="lane-header">
|
||||||
|
<span class="lane-title">미배치</span>
|
||||||
|
<span class="badge text-bg-warning badge-src">보관함</span>
|
||||||
|
</div>
|
||||||
|
<ul class="lane-body" id="unplacedBody"><!-- 렌더링 --></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 추가/수정 모달 -->
|
||||||
|
<div class="modal fade" id="menuModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modalTitle">메뉴 추가</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="modalMode" value="INSERT">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="fMenuId">메뉴 ID (kebab-case)</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="fMenuId" placeholder="ex) custom-link">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="fMenuName">노출명</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="fMenuName">
|
||||||
|
<div class="dflt-hint" id="hintMenuName"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="fMenuPath">Path (그룹은 생략 가능)</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="fMenuPath" placeholder="/example 또는 https://...">
|
||||||
|
<div class="dflt-hint" id="hintMenuPath"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="fIconClass">아이콘 클래스 (마이페이지용, ex: fa-users)</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="fIconClass">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="fMenuSection">섹션</label>
|
||||||
|
<select class="form-select form-select-sm" id="fMenuSection">
|
||||||
|
<option value="GNB">GNB (상단 메뉴)</option>
|
||||||
|
<option value="MYPAGE">마이페이지</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="fGroupYn">
|
||||||
|
<label class="form-check-label" for="fGroupYn">그룹(상위 메뉴)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="fNewWindowYn">
|
||||||
|
<label class="form-check-label" for="fNewWindowYn">새 창 열기</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">노출 권한 (미선택 = 전체 노출)</label>
|
||||||
|
<div class="role-check-group" id="exposeRoleChecks"></div>
|
||||||
|
<div class="dflt-hint" id="hintExposeRoles"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">접근 권한 (미선택 = 제한 없음)</label>
|
||||||
|
<div class="role-check-group" id="accessRoleChecks"></div>
|
||||||
|
<div class="dflt-hint" id="hintAccessRoles"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" level="W" onclick="submitModal()">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- end content_middle -->
|
||||||
|
</div><!-- end right_box -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -19,16 +19,42 @@
|
|||||||
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
|
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
|
||||||
var combo;
|
var combo;
|
||||||
|
|
||||||
function formatNoticeType(cellvalue, options, rowObject) {
|
// 코드 상수 — PortalNoticeUI 와 동기화
|
||||||
if (!combo || !combo.noticeTypeList) {
|
var NOTICE_TYPE_INCIDENT = '3';
|
||||||
return cellvalue;
|
|
||||||
|
// IncidentState 라벨 — 상세 화면과 동일하게 유지
|
||||||
|
var STATE_LABELS = {
|
||||||
|
'INVESTIGATING': '조사중',
|
||||||
|
'IDENTIFIED': '원인 파악',
|
||||||
|
'MONITORING': '모니터링',
|
||||||
|
'RESOLVED': '해소',
|
||||||
|
'CANCELED': '취소'
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatIncidentState(cellvalue, options, rowObject) {
|
||||||
|
if (!cellvalue) return '';
|
||||||
|
var label = STATE_LABELS[cellvalue] || cellvalue;
|
||||||
|
var closed = (cellvalue === 'RESOLVED' || cellvalue === 'CANCELED');
|
||||||
|
return closed
|
||||||
|
? '<span title="' + cellvalue + '">' + label + '</span>'
|
||||||
|
: '<span style="color: red;" title="' + cellvalue + '">' + label + '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatNoticeType(cellvalue, options, rowObject) {
|
||||||
|
var name = cellvalue;
|
||||||
|
if (combo && combo.noticeTypeList) {
|
||||||
for (var i = 0; i < combo.noticeTypeList.length; i++) {
|
for (var i = 0; i < combo.noticeTypeList.length; i++) {
|
||||||
if (combo.noticeTypeList[i].CODE == cellvalue) {
|
if (combo.noticeTypeList[i].CODE == cellvalue) {
|
||||||
return combo.noticeTypeList[i].NAME;
|
name = combo.noticeTypeList[i].NAME;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cellvalue;
|
}
|
||||||
|
// 장애인데 종료 시각이 없으면 아직 진행 중이다
|
||||||
|
if (cellvalue == NOTICE_TYPE_INCIDENT && rowObject && !rowObject.endAt) {
|
||||||
|
return '<span style="color: red;">' + name + '-진행중</span>';
|
||||||
|
}
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatuseYn(cellvalue, options, rowObject) {
|
function formatuseYn(cellvalue, options, rowObject) {
|
||||||
@@ -115,14 +141,15 @@
|
|||||||
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:60 },
|
{ name : 'rowNum' , align:'center', width:60 },
|
||||||
{ name : 'noticeType' , align:'center', width:60, formatter: formatNoticeType },
|
{ name : 'noticeType' , align:'center', width:100, formatter: formatNoticeType },
|
||||||
{ name : 'noticeSubject' , align:'left' , width:300, formatter: formatFile },
|
{ name : 'noticeSubject' , align:'left' , width:300, formatter: formatFile },
|
||||||
{ name : 'fixYn' , align:'center', width:60, formatter: formatFixYn },
|
{ 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 : 'state' , align:'center', width:90, sortable:false, formatter: formatIncidentState },
|
||||||
{ 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 },
|
||||||
{ name : 'inquirerName' , align:'center', width:100 },
|
{ name : 'inquirerName' , align:'center', width:100 },
|
||||||
|
|||||||
@@ -32,10 +32,35 @@
|
|||||||
// 영향 API 목록 (UI 상태)
|
// 영향 API 목록 (UI 상태)
|
||||||
var affectedApis = [];
|
var affectedApis = [];
|
||||||
|
|
||||||
|
// 장애 타임라인 (UI 상태)
|
||||||
|
var currentIncidentId = null;
|
||||||
|
var currentIncidentState = null;
|
||||||
|
|
||||||
|
// 종결 상태 — 재전이 불가 (재발은 새 장애로 등록)
|
||||||
|
var TERMINAL_STATES = ['RESOLVED', 'CANCELED'];
|
||||||
|
|
||||||
|
// IncidentState 라벨 — 상태 콤보 문구와 동일하게 유지
|
||||||
|
var STATE_LABELS = {
|
||||||
|
'INVESTIGATING': '조사중',
|
||||||
|
'IDENTIFIED': '원인 파악',
|
||||||
|
'MONITORING': '모니터링',
|
||||||
|
'RESOLVED': '해소',
|
||||||
|
'CANCELED': '취소'
|
||||||
|
};
|
||||||
|
|
||||||
function isIncidentType(t) {
|
function isIncidentType(t) {
|
||||||
return t === NOTICE_TYPE_INCIDENT || t === NOTICE_TYPE_MAINTENANCE;
|
return t === NOTICE_TYPE_INCIDENT || t === NOTICE_TYPE_MAINTENANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stateLabel(code) {
|
||||||
|
if (!code) return '';
|
||||||
|
return (STATE_LABELS[code] || code) + ' (' + code + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return $('<div>').text(text == null ? '' : text).html();
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
var key = "${param.id}";
|
var key = "${param.id}";
|
||||||
isDetail = key != "" && key != "null";
|
isDetail = key != "" && key != "null";
|
||||||
@@ -76,12 +101,28 @@
|
|||||||
// INCIDENT 만 상태 영역 표시
|
// INCIDENT 만 상태 영역 표시
|
||||||
if (t === NOTICE_TYPE_INCIDENT) {
|
if (t === NOTICE_TYPE_INCIDENT) {
|
||||||
$('.incident-state-row').show();
|
$('.incident-state-row').show();
|
||||||
|
$('.incident-endat-hint').show();
|
||||||
} else {
|
} else {
|
||||||
$('.incident-state-row').hide();
|
$('.incident-state-row').hide();
|
||||||
|
$('.incident-endat-hint').hide();
|
||||||
}
|
}
|
||||||
|
// 장애는 종결(해소·취소) 후에만 종료 시각을 손볼 수 있다. 진행 중에는 상태를 따른다.
|
||||||
|
var closed = $.inArray(currentIncidentState, TERMINAL_STATES) >= 0;
|
||||||
|
$('#endAt').prop('readonly', t === NOTICE_TYPE_INCIDENT && !closed);
|
||||||
|
$('.incident-endat-hint').text(closed
|
||||||
|
? '※ 종결된 장애입니다. 실제 복구 시각으로 수정할 수 있습니다.'
|
||||||
|
: '※ 타임라인에서 해소·취소로 전이하면 자동 입력됩니다.');
|
||||||
} else {
|
} else {
|
||||||
$('.incident-row').hide();
|
$('.incident-row').hide();
|
||||||
$('.incident-state-row').hide();
|
$('.incident-state-row').hide();
|
||||||
|
$('.incident-endat-hint').hide();
|
||||||
|
$('#endAt').prop('readonly', false);
|
||||||
|
}
|
||||||
|
// 타임라인은 등록된 장애(수정 모드)에서만 다룬다
|
||||||
|
if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) {
|
||||||
|
$('#timelineSection').show();
|
||||||
|
} else {
|
||||||
|
$('#timelineSection').hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +233,150 @@
|
|||||||
renderAffectedApis();
|
renderAffectedApis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ───────────────────── 장애 처리 타임라인 ─────────────────────
|
||||||
|
|
||||||
|
/** 현재 상태 기준으로 전이 가능한 선택지만 남긴다. 실제 차단은 서버가 한다. */
|
||||||
|
function applyStateChangeGuard() {
|
||||||
|
var terminal = $.inArray(currentIncidentState, TERMINAL_STATES) >= 0;
|
||||||
|
|
||||||
|
$('#timelineStateChange').prop('disabled', terminal);
|
||||||
|
if (terminal) {
|
||||||
|
$('#timelineStateChange').prop('checked', false);
|
||||||
|
$('#timelineNextState').prop('disabled', true).val('');
|
||||||
|
}
|
||||||
|
$('#timelineClosedHint').toggle(terminal);
|
||||||
|
$('#timelineResolveHint').toggle(!terminal);
|
||||||
|
|
||||||
|
// 현재와 동일한 상태로는 전이할 수 없다
|
||||||
|
$('#timelineNextState option').each(function() {
|
||||||
|
var v = $(this).val();
|
||||||
|
$(this).prop('disabled', v !== '' && v === currentIncidentState);
|
||||||
|
});
|
||||||
|
if ($('#timelineNextState').val() === currentIncidentState) {
|
||||||
|
$('#timelineNextState').val('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTimeline() {
|
||||||
|
if (!currentIncidentId) {
|
||||||
|
renderTimeline([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: "POST", url: url, dataType: "json",
|
||||||
|
data: {cmd: 'TIMELINE_LIST', incidentId: currentIncidentId},
|
||||||
|
success: function (rows) { renderTimeline(rows || []); },
|
||||||
|
error: function (e) { alert(e.responseText); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTimeline(rows) {
|
||||||
|
var $tb = $('#timelineTbody');
|
||||||
|
$tb.empty();
|
||||||
|
if (!rows.length) {
|
||||||
|
$tb.append('<tr><td colspan="6" style="text-align:center;color:#999;">등록된 타임라인이 없습니다.</td></tr>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.each(rows, function(_, row) {
|
||||||
|
var author = row.authorType === 'SYSTEM' ? 'System' : 'Admin';
|
||||||
|
var visible = row.visibleYn === 'Y';
|
||||||
|
var $tr = $('<tr>');
|
||||||
|
$tr.append('<td style="text-align:center;white-space:nowrap;">' + escapeHtml(row.eventAt) + '</td>');
|
||||||
|
$tr.append('<td style="text-align:center;">' + author + '</td>');
|
||||||
|
$tr.append('<td style="text-align:center;white-space:nowrap;">' + (row.stateAfter ? escapeHtml(stateLabel(row.stateAfter)) : '-') + '</td>');
|
||||||
|
$tr.append('<td>' + escapeHtml(row.body).replace(/\n/g, '<br>') + '</td>');
|
||||||
|
|
||||||
|
// 현재 상태(라벨) + 눌렀을 때 바뀔 상태(버튼) 를 나눠 표기 — 버튼 하나면 의미가 모호하다
|
||||||
|
var $visibleTd = $('<td style="text-align:center;white-space:nowrap;">');
|
||||||
|
$('<span>')
|
||||||
|
.text(visible ? '공개중' : '비공개')
|
||||||
|
.css({color: visible ? '#1B8C4A' : '#999', 'margin-right': '5px'})
|
||||||
|
.appendTo($visibleTd);
|
||||||
|
$('<button type="button" class="cssbtn" level="W" status="DETAIL" style="min-width:0;padding:3px 8px;">')
|
||||||
|
.text(visible ? '비공개로' : '공개로')
|
||||||
|
.click(function() { toggleTimelineVisible(row.timelineId, visible ? 'N' : 'Y'); })
|
||||||
|
.appendTo($visibleTd);
|
||||||
|
$tr.append($visibleTd);
|
||||||
|
|
||||||
|
var $deleteTd = $('<td style="text-align:center;">');
|
||||||
|
$('<button type="button" class="cssbtn smallBtn" level="W" status="DETAIL" style="min-width:0;width:90%;">')
|
||||||
|
.text('삭제')
|
||||||
|
.click(function() { deleteTimeline(row.timelineId); })
|
||||||
|
.appendTo($deleteTd);
|
||||||
|
$tr.append($deleteTd);
|
||||||
|
|
||||||
|
$tb.append($tr);
|
||||||
|
});
|
||||||
|
buttonControl(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTimeline() {
|
||||||
|
if (!currentIncidentId) {
|
||||||
|
alert('장애 정보를 먼저 저장하여 주십시오.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var body = $('#timelineBody').val();
|
||||||
|
if (!body || !$.trim(body)) {
|
||||||
|
alert('타임라인 메세지를 입력하여 주십시오.');
|
||||||
|
$('#timelineBody').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var stateChange = $('#timelineStateChange').is(':checked');
|
||||||
|
var nextState = $('#timelineNextState').val();
|
||||||
|
if (stateChange && !nextState) {
|
||||||
|
alert('변경할 상태를 선택하여 주십시오.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: "POST", url: url, dataType: "json",
|
||||||
|
data: {cmd: 'TIMELINE_ADD', incidentId: currentIncidentId, body: body,
|
||||||
|
stateChange: stateChange, nextState: nextState},
|
||||||
|
success: function (data) {
|
||||||
|
$('#timelineBody').val('');
|
||||||
|
$('#timelineStateChange').prop('checked', false).trigger('change');
|
||||||
|
// 상태 전이 시 서버가 상태/이전 상태/종료 시각을 함께 갱신한다
|
||||||
|
$('#stateText').text(data.state ? stateLabel(data.state) : '');
|
||||||
|
$('#previousState').text(data.previousState ? stateLabel(data.previousState) : '없음');
|
||||||
|
$('#endAt').val(data.endAt || '');
|
||||||
|
currentIncidentState = data.state || null;
|
||||||
|
applyStateChangeGuard();
|
||||||
|
// 종결 전이 직후부터 종료 시각을 손볼 수 있어야 한다
|
||||||
|
toggleIncidentFields();
|
||||||
|
loadTimeline();
|
||||||
|
},
|
||||||
|
error: function (e) { alert(e.responseText); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTimelineVisible(timelineId, visibleYn) {
|
||||||
|
$.ajax({
|
||||||
|
type: "POST", url: url,
|
||||||
|
data: {cmd: 'TIMELINE_VISIBLE', timelineId: timelineId, visibleYn: visibleYn},
|
||||||
|
success: loadTimeline,
|
||||||
|
error: function (e) { alert(e.responseText); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTimeline(timelineId) {
|
||||||
|
if (confirm("<%= localeMessage.getString("common.checkDelete")%>") != true) return;
|
||||||
|
$.ajax({
|
||||||
|
type: "POST", url: url,
|
||||||
|
data: {cmd: 'TIMELINE_DELETE', timelineId: timelineId},
|
||||||
|
success: loadTimeline,
|
||||||
|
error: function (e) { alert(e.responseText); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 장애/점검 공지는 최초 게시 후 게시유형을 바꿀 수 없다. */
|
||||||
|
function lockNoticeType(noticeType) {
|
||||||
|
if (!isIncidentType(noticeType)) return;
|
||||||
|
$('#noticeType').prop('disabled', true);
|
||||||
|
if (!$('#noticeTypeHidden').length) {
|
||||||
|
$('#ajaxForm').append('<input type="hidden" id="noticeTypeHidden" name="noticeType">');
|
||||||
|
}
|
||||||
|
$('#noticeTypeHidden').val(noticeType);
|
||||||
|
}
|
||||||
|
|
||||||
function detail(key) {
|
function detail(key) {
|
||||||
if (!isDetail) return;
|
if (!isDetail) return;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -207,17 +392,28 @@
|
|||||||
var decodedContent = decodeHTMLEntities(data.noticeDetail);
|
var decodedContent = decodeHTMLEntities(data.noticeDetail);
|
||||||
$('#contents').summernote('code', decodedContent);
|
$('#contents').summernote('code', decodedContent);
|
||||||
|
|
||||||
|
// 게시유형은 최초 저장 후 변경 불가 (disabled 는 전송되지 않아 hidden 으로 값을 유지)
|
||||||
|
lockNoticeType(data.noticeType);
|
||||||
|
|
||||||
// 장애/점검 필드
|
// 장애/점검 필드
|
||||||
$('#summary').val(data.summary || '');
|
$('#summary').val(data.summary || '');
|
||||||
$('#startedAt').val(data.startedAt || '');
|
$('#startedAt').val(data.startedAt || '');
|
||||||
$('#endAt').val(data.endAt || '');
|
$('#endAt').val(data.endAt || '');
|
||||||
$('#state').val(data.state || 'INVESTIGATING');
|
// 상태/이전 상태는 타임라인 게시로만 바뀐다 (직접 수정 불가)
|
||||||
$('#previousState').text(data.previousState || '없음');
|
$('#stateText').text(data.state ? stateLabel(data.state) : '');
|
||||||
|
$('#previousState').text(data.previousState ? stateLabel(data.previousState) : '없음');
|
||||||
affectedApis = (data.affectedApis || []).map(function(a){
|
affectedApis = (data.affectedApis || []).map(function(a){
|
||||||
return {apiId: a.apiId, apiName: a.apiName || ''};
|
return {apiId: a.apiId, apiName: a.apiName || ''};
|
||||||
});
|
});
|
||||||
renderAffectedApis();
|
renderAffectedApis();
|
||||||
|
|
||||||
|
currentIncidentId = data.incidentId || null;
|
||||||
|
currentIncidentState = data.state || null;
|
||||||
|
applyStateChangeGuard();
|
||||||
toggleIncidentFields();
|
toggleIncidentFields();
|
||||||
|
if (data.noticeType === NOTICE_TYPE_INCIDENT && currentIncidentId) {
|
||||||
|
loadTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
if (data.fileInfo) {
|
if (data.fileInfo) {
|
||||||
fileInfo = {
|
fileInfo = {
|
||||||
@@ -245,6 +441,13 @@
|
|||||||
$('#btn_addAffectedApi').click(openApiPopup);
|
$('#btn_addAffectedApi').click(openApiPopup);
|
||||||
$('#btn_removeAffectedApi').click(removeSelectedAffectedApis);
|
$('#btn_removeAffectedApi').click(removeSelectedAffectedApis);
|
||||||
|
|
||||||
|
$('#timelineStateChange').on('change', function() {
|
||||||
|
var checked = $(this).is(':checked');
|
||||||
|
$('#timelineNextState').prop('disabled', !checked);
|
||||||
|
if (!checked) $('#timelineNextState').val('');
|
||||||
|
});
|
||||||
|
$('#btn_addTimeline').click(addTimeline);
|
||||||
|
|
||||||
$('#fileInput').change(function(e) {
|
$('#fileInput').change(function(e) {
|
||||||
var file = e.target.files[0];
|
var file = e.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -273,6 +476,11 @@
|
|||||||
alert('점검 종료 시각을 입력해 주십시오.');
|
alert('점검 종료 시각을 입력해 주십시오.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// datetime-local 값은 'YYYY-MM-DDTHH:mm' 이라 문자열 비교로 선후가 가려진다
|
||||||
|
if ($('#endAt').val() && $('#endAt').val() < $('#startedAt').val()) {
|
||||||
|
alert('종료 시각은 시작 시각보다 빠를 수 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (affectedApis.length === 0) {
|
if (affectedApis.length === 0) {
|
||||||
alert('영향 API를 1건 이상 선택해 주십시오.');
|
alert('영향 API를 1건 이상 선택해 주십시오.');
|
||||||
return;
|
return;
|
||||||
@@ -394,21 +602,15 @@
|
|||||||
<th>종료</th>
|
<th>종료</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="datetime-local" id="endAt" name="endAt" style="width:90%" placeholder="해소시 자동 채움">
|
<input type="datetime-local" id="endAt" name="endAt" style="width:90%" placeholder="해소시 자동 채움">
|
||||||
</td>
|
<div class="incident-endat-hint" style="display:none;color:#888;font-size:12px;margin-top:3px;">
|
||||||
</tr>
|
※ 타임라인에서 해소·취소로 전이하면 자동 입력됩니다.
|
||||||
<tr class="incident-row incident-state-row" style="display:none;">
|
|
||||||
<th>상태</th>
|
|
||||||
<td>
|
|
||||||
<div class="select-style">
|
|
||||||
<select id="state" name="state">
|
|
||||||
<option value="INVESTIGATING">INVESTIGATING - 조사중</option>
|
|
||||||
<option value="IDENTIFIED">IDENTIFIED - 원인 파악</option>
|
|
||||||
<option value="MONITORING">MONITORING - 모니터링</option>
|
|
||||||
<option value="RESOLVED">RESOLVED - 해소</option>
|
|
||||||
<option value="CANCELED">CANCELED - 취소</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- 상태/이전 상태는 타임라인 게시로만 전이한다 (직접 수정 불가) -->
|
||||||
|
<tr class="incident-row incident-state-row" style="display:none;">
|
||||||
|
<th>상태</th>
|
||||||
|
<td><span id="stateText"></span></td>
|
||||||
<th>이전 상태</th>
|
<th>이전 상태</th>
|
||||||
<td><span id="previousState">없음</span></td>
|
<td><span id="previousState">없음</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -435,6 +637,61 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- 타임라인은 [수정] 저장과 별개로 게시 즉시 반영되므로 폼 밖에 둔다 -->
|
||||||
|
<div id="timelineSection" style="display:none;margin-top:20px;">
|
||||||
|
<div class="title">타임라인</div>
|
||||||
|
<table class="table_row" cellspacing="0" style="width:100%;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 130px"/>
|
||||||
|
<col style="width: 80px"/>
|
||||||
|
<col style="width: 140px"/>
|
||||||
|
<col/>
|
||||||
|
<col style="width: 160px"/>
|
||||||
|
<col style="width: 80px"/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>시간</th>
|
||||||
|
<th>작성자</th>
|
||||||
|
<th>상태</th>
|
||||||
|
<th>메세지</th>
|
||||||
|
<th>개발자포탈 공개</th>
|
||||||
|
<th>삭제</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="timelineTbody">
|
||||||
|
<tr><td colspan="6" style="text-align:center;color:#999;">등록된 타임라인이 없습니다.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="timelineStateChange"> 상태 변경
|
||||||
|
</label>
|
||||||
|
<div class="select-style" style="width:220px;">
|
||||||
|
<select id="timelineNextState" disabled>
|
||||||
|
<option value="">(현재 상태 유지)</option>
|
||||||
|
<option value="INVESTIGATING">INVESTIGATING - 조사중</option>
|
||||||
|
<option value="IDENTIFIED">IDENTIFIED - 원인 파악</option>
|
||||||
|
<option value="MONITORING">MONITORING - 모니터링</option>
|
||||||
|
<option value="RESOLVED">RESOLVED - 해소</option>
|
||||||
|
<option value="CANCELED">CANCELED - 취소</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<span id="timelineResolveHint" style="color:#888;font-size:12px;">※ 해소(RESOLVED)·취소(CANCELED)로 전이하면 종료 시각이 자동 입력됩니다.</span>
|
||||||
|
<span id="timelineClosedHint" style="display:none;color:#C0392B;font-size:12px;">
|
||||||
|
※ 종결된 장애입니다. 상태 변경 불가 — 재발 시 새 장애로 등록하십시오.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:5px;display:flex;gap:5px;">
|
||||||
|
<textarea id="timelineBody" style="flex:1;height:60px;"
|
||||||
|
placeholder="타임라인 메세지 입력 예 : 모니터링 5분간 정상 응답 확인 후 완료 상태 전이 예정"></textarea>
|
||||||
|
<button type="button" class="cssbtn" id="btn_addTimeline" level="W" status="DETAIL">
|
||||||
|
<i class="material-icons">send</i> 게시
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
var url = '<c:url value="/onl/apim/portalproperty/propertyMan.json" />';
|
var url = '<c:url value="/onl/apim/portalproperty/propertyMan.json" />';
|
||||||
var url_view = '<c:url value="/onl/apim/portalproperty/propertyMan.view" />';
|
var url_view = '<c:url value="/onl/apim/portalproperty/propertyMan.view" />';
|
||||||
var lastsel2;
|
var lastsel2;
|
||||||
|
var clickedCol; //클릭한 셀의 컬럼 인덱스(포커스 이동용)
|
||||||
|
|
||||||
function isValid() {
|
function isValid() {
|
||||||
if ($('input[name=prptyGroupName]').val() == "") {
|
if ($('input[name=prptyGroupName]').val() == "") {
|
||||||
@@ -86,9 +87,11 @@
|
|||||||
editurl: "clientArray",
|
editurl: "clientArray",
|
||||||
colNames: ['<%= localeMessage.getString("propertyDetail.propertyKey") %>',
|
colNames: ['<%= localeMessage.getString("propertyDetail.propertyKey") %>',
|
||||||
'<%= localeMessage.getString("propertyDetail.propertyValue") %>',
|
'<%= localeMessage.getString("propertyDetail.propertyValue") %>',
|
||||||
|
'설명',
|
||||||
'<%= localeMessage.getString("propertyDetail.delYn") %>'],
|
'<%= localeMessage.getString("propertyDetail.delYn") %>'],
|
||||||
colModel: [{name: 'PRPTYNAME', width: 50, align: 'left', editable: true},
|
colModel: [{name: 'PRPTYNAME', width: 50, align: 'left', editable: true},
|
||||||
{name: 'PRPTY2VAL', width: 200, align: 'left', editable: true},
|
{name: 'PRPTY2VAL', width: 150, align: 'left', editable: true},
|
||||||
|
{name: 'PRPTYDESC', width: 200, align: 'left', editable: true, edittype: 'textarea', editoptions: {rows: 2}},
|
||||||
{
|
{
|
||||||
name: 'DELETEYN',
|
name: 'DELETEYN',
|
||||||
width: 20,
|
width: 20,
|
||||||
@@ -101,6 +104,10 @@
|
|||||||
},
|
},
|
||||||
loadComplete: function () {
|
loadComplete: function () {
|
||||||
},
|
},
|
||||||
|
onCellSelect: function (rowid, iCol) {
|
||||||
|
//onSelectRow보다 먼저 발화 → 클릭한 컬럼 인덱스 저장
|
||||||
|
clickedCol = iCol;
|
||||||
|
},
|
||||||
onSelectRow: function (rowid, status) {
|
onSelectRow: function (rowid, status) {
|
||||||
if (lastsel2 != undefined) {
|
if (lastsel2 != undefined) {
|
||||||
if ($("#grid tr#" + lastsel2).attr("editable") == 1) { //editable=1 means row in edit mode
|
if ($("#grid tr#" + lastsel2).attr("editable") == 1) { //editable=1 means row in edit mode
|
||||||
@@ -108,7 +115,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$('#grid').restoreRow(lastsel2);
|
$('#grid').restoreRow(lastsel2);
|
||||||
$('#grid').editRow(rowid, true);
|
//focusField로 클릭한 셀에 포커스(미지정 시 첫 편집 컬럼으로 감)
|
||||||
|
$('#grid').editRow(rowid, {keys: true, focusField: clickedCol});
|
||||||
lastsel2 = rowid;
|
lastsel2 = rowid;
|
||||||
},
|
},
|
||||||
onSortCol: function () {
|
onSortCol: function () {
|
||||||
@@ -258,6 +266,7 @@
|
|||||||
var data = new Object();
|
var data = new Object();
|
||||||
data["PRPTYNAME"] = $("input[name=prptyName]").val();
|
data["PRPTYNAME"] = $("input[name=prptyName]").val();
|
||||||
data["PRPTY2VAL"] = $("input[name=prpty2Val]").val();
|
data["PRPTY2VAL"] = $("input[name=prpty2Val]").val();
|
||||||
|
data["PRPTYDESC"] = $("input[name=prptyDesc]").val();
|
||||||
|
|
||||||
var rows = $("#grid")[0].rows;
|
var rows = $("#grid")[0].rows;
|
||||||
var index = Number($(rows[rows.length - 1]).attr("id"));
|
var index = Number($(rows[rows.length - 1]).attr("id"));
|
||||||
@@ -340,6 +349,11 @@
|
|||||||
<td colspan="3"><input type="text" name="prpty2Val"/>
|
<td colspan="3"><input type="text" name="prpty2Val"/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>설명</th>
|
||||||
|
<td colspan="3"><input type="text" name="prptyDesc"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<!-- grid -->
|
<!-- grid -->
|
||||||
<table id="grid"></table>
|
<table id="grid"></table>
|
||||||
|
|||||||
@@ -285,7 +285,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fullLoginId = id + "@" + domain;
|
const fullLoginId = id + "@" + domain;
|
||||||
const emailPattern = /^[A-Za-z0-9._-]*@[A-Za-z0-9][-A-Za-z0-9.]+\.[A-Za-z]{2,}$/;
|
const emailPattern = /^[A-Za-z0-9._%+-]*@[A-Za-z0-9][-A-Za-z0-9.]+\.[A-Za-z]{2,}$/;
|
||||||
if (!emailPattern.test(fullLoginId)) {
|
if (!emailPattern.test(fullLoginId)) {
|
||||||
alert(fullLoginId + " 은 올바른 이메일 형식이 아닙니다.");
|
alert(fullLoginId + " 은 올바른 이메일 형식이 아닙니다.");
|
||||||
return;
|
return;
|
||||||
@@ -317,7 +317,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateEmailId(input) {
|
function validateEmailId(input) {
|
||||||
input.value = input.value.replace(/[^A-Za-z0-9._-]/g, '');
|
input.value = input.value.replace(/[^A-Za-z0-9._%+-]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateDomainInput(input) {
|
function validateDomainInput(input) {
|
||||||
@@ -531,8 +531,8 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 신규 등록: 사유 불필요
|
// 신규 등록: 사유 불필요. 초기 패스워드는 비밀번호 초기화와 동일(이메일ID + subfix)
|
||||||
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
|
if (!confirm("가입자를 등록하시겠습니까?\n초기 패스워드는 '이메일ID${passwordInitSubfix}' 입니다.")) return;
|
||||||
submitUser("INSERT", null);
|
submitUser("INSERT", null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -567,7 +567,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#btn_password_reset").click(function () {
|
$("#btn_password_reset").click(function () {
|
||||||
if (!confirm("패스워드를 초기화 하시겠습니까?\n초기 패스워드는 '!' + 이메일ID 입니다.")) return;
|
if (!confirm("패스워드를 초기화 하시겠습니까?\n초기 패스워드는 '이메일ID${passwordInitSubfix}' 입니다.")) return;
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ $(document).ready(function() {
|
|||||||
<form id="ajaxForm">
|
<form id="ajaxForm">
|
||||||
<input type="hidden" name="id">
|
<input type="hidden" name="id">
|
||||||
<table class="table_row" cellspacing="0">
|
<table class="table_row" cellspacing="0">
|
||||||
|
<colgroup>
|
||||||
|
<col width="200px">
|
||||||
|
</colgroup>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%= localeMessage.getString("messageTemplate.messageCode") %> <font color="red"> *</font></th>
|
<th><%= localeMessage.getString("messageTemplate.messageCode") %> <font color="red"> *</font></th>
|
||||||
<td><input type="text" name="messageCode" data-required data-warning="메세지 코드는 필수입니다"/></td>
|
<td><input type="text" name="messageCode" data-required data-warning="메세지 코드는 필수입니다"/></td>
|
||||||
|
|||||||
@@ -66,22 +66,27 @@
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: '{b}: {c} ({d}%)'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'vertical',
|
||||||
bottom: 10,
|
right: 30,
|
||||||
|
top: 'middle',
|
||||||
data: ['성공', 'Timeout', '시스템오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['38%', '65%'],
|
||||||
center: ['50%', '50%'],
|
center: ['44%', '55%'],
|
||||||
avoidLabelOverlap: true,
|
avoidLabelOverlap: true,
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: '{b}: {c}'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
label: {
|
label: {
|
||||||
@@ -98,18 +103,6 @@
|
|||||||
{ 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' } }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,7 +112,7 @@
|
|||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||||
@@ -191,13 +184,9 @@
|
|||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' }
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: grandTotal.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||||
|
|
||||||
|
|
||||||
// 호출량 차트 업데이트
|
// 호출량 차트 업데이트
|
||||||
@@ -455,6 +444,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -501,6 +493,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -587,10 +582,18 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="title" id="title">API 일별 통계</div>
|
<div class="title" id="title">API 일별 통계</div>
|
||||||
<table class="search_condition" cellspacing="0">
|
<table class="search_condition" cellspacing="0" style="table-layout:fixed;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">조회기간</th>
|
<th>조회기간</th>
|
||||||
<td colspan="5">
|
<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;">
|
||||||
~
|
~
|
||||||
@@ -599,29 +602,29 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">API명</th>
|
<th>API명</th>
|
||||||
<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>
|
<th>업무구분</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
||||||
</td>
|
</td>
|
||||||
<th style="width:100px;">인스턴스</th>
|
<th>인스턴스</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;">클라이언트ID</th>
|
<th>클라이언트ID</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchClientId" value="${param.searchClientId}">
|
<input type="text" name="searchClientId" value="${param.searchClientId}">
|
||||||
</td>
|
</td>
|
||||||
<th style="width:100px;">Inbound Adapter</th>
|
<th>Inbound Adapter</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
|
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
|
||||||
</td>
|
</td>
|
||||||
<th style="width:100px;">Outbound Adapter</th>
|
<th>Outbound Adapter</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
||||||
</td>
|
</td>
|
||||||
@@ -630,7 +633,10 @@
|
|||||||
</table>
|
</table>
|
||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div class="chart" style="position:relative; padding:0;">
|
||||||
|
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||||
|
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||||
|
</div>
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -67,22 +67,27 @@
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: '{b}: {c} ({d}%)'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'vertical',
|
||||||
bottom: 10,
|
right: 30,
|
||||||
|
top: 'middle',
|
||||||
data: ['성공', 'Timeout', '시스템오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['38%', '65%'],
|
||||||
center: ['50%', '50%'],
|
center: ['44%', '55%'],
|
||||||
avoidLabelOverlap: true,
|
avoidLabelOverlap: true,
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: '{b}: {c}'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
label: {
|
label: {
|
||||||
@@ -99,18 +104,6 @@
|
|||||||
{ 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' } }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,7 +113,7 @@
|
|||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||||
@@ -227,13 +220,9 @@
|
|||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' }
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: grandTotal.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||||
|
|
||||||
|
|
||||||
// 호출량 차트 업데이트
|
// 호출량 차트 업데이트
|
||||||
@@ -457,6 +446,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -503,6 +495,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -567,41 +562,47 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="title" id="title">API 시간별 통계</div>
|
<div class="title" id="title">API 시간별 통계</div>
|
||||||
<table class="search_condition" cellspacing="0">
|
<table class="search_condition" cellspacing="0" style="table-layout:fixed;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">조회일자</th>
|
<th>조회일자</th>
|
||||||
<td colspan="3">
|
<td colspan="5">
|
||||||
<input type="text" name="searchDate" value="${param.searchDate}" style="width:100px;">
|
<input type="text" name="searchDate" value="${param.searchDate}" style="width:100px;">
|
||||||
<span style="color:#888; font-size:12px; margin-left:10px;">(선택일 00시 ~ 23시 조회)</span>
|
<span style="color:#888; font-size:12px; margin-left:10px;">(선택일 00시 ~ 23시 조회)</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">API명</th>
|
<th>API명</th>
|
||||||
<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>
|
<th>업무구분</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
||||||
|
</td>
|
||||||
|
<th>인스턴스</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>
|
<th>클라이언트ID</th>
|
||||||
<td>
|
|
||||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
|
||||||
</td>
|
|
||||||
<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>
|
<th>Inbound Adapter</th>
|
||||||
<tr>
|
|
||||||
<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}">
|
||||||
</td>
|
</td>
|
||||||
<th style="width:100px;">Outbound Adapter</th>
|
<th>Outbound Adapter</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
||||||
</td>
|
</td>
|
||||||
@@ -610,7 +611,10 @@
|
|||||||
</table>
|
</table>
|
||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div class="chart" style="position:relative; padding:0;">
|
||||||
|
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||||
|
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||||
|
</div>
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -59,22 +59,27 @@
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: '{b}: {c} ({d}%)'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'vertical',
|
||||||
bottom: 10,
|
right: 30,
|
||||||
|
top: 'middle',
|
||||||
data: ['성공', 'Timeout', '시스템오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['38%', '65%'],
|
||||||
center: ['50%', '50%'],
|
center: ['44%', '55%'],
|
||||||
avoidLabelOverlap: true,
|
avoidLabelOverlap: true,
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: '{b}: {c}'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
label: {
|
label: {
|
||||||
@@ -91,18 +96,6 @@
|
|||||||
{ 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' } }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,7 +105,7 @@
|
|||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||||
@@ -189,13 +182,9 @@
|
|||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' }
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: grandTotal.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||||
|
|
||||||
|
|
||||||
// 호출량 차트 업데이트
|
// 호출량 차트 업데이트
|
||||||
@@ -470,6 +459,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -516,6 +508,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -619,7 +614,10 @@
|
|||||||
</table>
|
</table>
|
||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div class="chart" style="position:relative; padding:0;">
|
||||||
|
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||||
|
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||||
|
</div>
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -636,7 +634,17 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -66,22 +66,27 @@
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: '{b}: {c} ({d}%)'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'vertical',
|
||||||
bottom: 10,
|
right: 30,
|
||||||
|
top: 'middle',
|
||||||
data: ['성공', 'Timeout', '시스템오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['38%', '65%'],
|
||||||
center: ['50%', '50%'],
|
center: ['44%', '55%'],
|
||||||
avoidLabelOverlap: true,
|
avoidLabelOverlap: true,
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: '{b}: {c}'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
label: {
|
label: {
|
||||||
@@ -98,18 +103,6 @@
|
|||||||
{ 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' } }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,7 +112,7 @@
|
|||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||||
@@ -189,13 +182,9 @@
|
|||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' }
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: grandTotal.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -460,6 +449,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -506,6 +498,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -578,11 +573,19 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="title" id="title">API 월별 통계</div>
|
<div class="title" id="title">API 월별 통계</div>
|
||||||
<table class="search_condition" cellspacing="0">
|
<table class="search_condition" cellspacing="0" style="table-layout:fixed;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">조회기간</th>
|
<th>조회기간</th>
|
||||||
<td colspan="3">
|
<td colspan="5">
|
||||||
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;" placeholder="YYYY-MM">
|
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;" placeholder="YYYY-MM">
|
||||||
~
|
~
|
||||||
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;" placeholder="YYYY-MM">
|
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;" placeholder="YYYY-MM">
|
||||||
@@ -590,31 +593,29 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">API명</th>
|
<th>API명</th>
|
||||||
<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>
|
<th>업무구분</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
||||||
|
</td>
|
||||||
|
<th>인스턴스</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>
|
<th>클라이언트ID</th>
|
||||||
<td>
|
|
||||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
|
||||||
</td>
|
|
||||||
<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>
|
<th>Inbound Adapter</th>
|
||||||
<tr>
|
|
||||||
<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}">
|
||||||
</td>
|
</td>
|
||||||
<th style="width:100px;">Outbound Adapter</th>
|
<th>Outbound Adapter</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
||||||
</td>
|
</td>
|
||||||
@@ -623,7 +624,10 @@
|
|||||||
</table>
|
</table>
|
||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div class="chart" style="position:relative; padding:0;">
|
||||||
|
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||||
|
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||||
|
</div>
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -66,22 +66,27 @@
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: '{b}: {c} ({d}%)'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'vertical',
|
||||||
bottom: 10,
|
right: 30,
|
||||||
|
top: 'middle',
|
||||||
data: ['성공', 'Timeout', '시스템오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['38%', '65%'],
|
||||||
center: ['50%', '50%'],
|
center: ['44%', '55%'],
|
||||||
avoidLabelOverlap: true,
|
avoidLabelOverlap: true,
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: '{b}: {c}'
|
formatter: function (params) {
|
||||||
|
return params.name + ': ' + params.value.toLocaleString();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
label: {
|
label: {
|
||||||
@@ -98,18 +103,6 @@
|
|||||||
{ 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' } }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,7 +112,7 @@
|
|||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||||
@@ -191,13 +184,9 @@
|
|||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' }
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
]
|
]
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: grandTotal.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -436,6 +425,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -482,6 +474,9 @@
|
|||||||
for (var i = 0; i < colModel.length; i++) {
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
}
|
}
|
||||||
|
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||||
|
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||||
|
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -549,42 +544,48 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="title" id="title">API 연간 통계</div>
|
<div class="title" id="title">API 연간 통계</div>
|
||||||
<table class="search_condition" cellspacing="0">
|
<table class="search_condition" cellspacing="0" style="table-layout:fixed;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
<col style="width:150px;">
|
||||||
|
<col>
|
||||||
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">조회기간</th>
|
<th>조회기간</th>
|
||||||
<td colspan="3">
|
<td colspan="5">
|
||||||
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;" placeholder="YYYY">
|
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;" placeholder="YYYY">
|
||||||
~
|
~
|
||||||
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;" placeholder="YYYY">
|
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;" placeholder="YYYY">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">API명</th>
|
<th>API명</th>
|
||||||
<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>
|
<th>업무구분</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
||||||
|
</td>
|
||||||
|
<th>인스턴스</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>
|
<th>클라이언트ID</th>
|
||||||
<td>
|
|
||||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
|
||||||
</td>
|
|
||||||
<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>
|
<th>Inbound Adapter</th>
|
||||||
<tr>
|
|
||||||
<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}">
|
||||||
</td>
|
</td>
|
||||||
<th style="width:100px;">Outbound Adapter</th>
|
<th>Outbound Adapter</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
||||||
</td>
|
</td>
|
||||||
@@ -593,7 +594,10 @@
|
|||||||
</table>
|
</table>
|
||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div class="chart" style="position:relative; padding:0;">
|
||||||
|
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||||
|
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||||
|
</div>
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,10 @@
|
|||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("input[name=searchType]").click(function() {
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
|
||||||
$("#btn_excel").click(function() {
|
$("#btn_excel").click(function() {
|
||||||
exportToExcel();
|
exportToExcel();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -76,6 +76,15 @@
|
|||||||
return cellvalue.substring(1, 4);
|
return cellvalue.substring(1, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function apiStatusFormatter(cellvalue, options, rowObject) {
|
||||||
|
switch (cellvalue) {
|
||||||
|
case 'E': return '장애';
|
||||||
|
case 'D': return '지연';
|
||||||
|
case 'C': return '점검';
|
||||||
|
default: return '정상';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function list() {
|
function list() {
|
||||||
detail()
|
detail()
|
||||||
var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로
|
var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로
|
||||||
@@ -90,6 +99,7 @@
|
|||||||
'API FULL PATH',
|
'API FULL PATH',
|
||||||
'요청',
|
'요청',
|
||||||
'응답',
|
'응답',
|
||||||
|
'상태',
|
||||||
'작성자',
|
'작성자',
|
||||||
'가상응답여부',
|
'가상응답여부',
|
||||||
'변경일시(*)',
|
'변경일시(*)',
|
||||||
@@ -99,11 +109,12 @@
|
|||||||
{name: 'eaiSvcName', align: 'left', width: '100', sortable: true},
|
{name: 'eaiSvcName', align: 'left', width: '100', sortable: true},
|
||||||
{name: 'eaiSvcDesc', align: 'left', sortable: false},
|
{name: 'eaiSvcDesc', align: 'left', sortable: false},
|
||||||
{name: 'apiFullPath', align: 'left', sortable: false},
|
{name: 'apiFullPath', align: 'left', sortable: false},
|
||||||
{name: 'fromAdapter', align: 'center', width: '40', formatter: adapterNameShortFormatter, sortable: false},
|
{name: 'fromAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
|
||||||
{name: 'toAdapter', align: 'center', width: '40', formatter: adapterNameShortFormatter, sortable: false},
|
{name: 'toAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
|
||||||
{name: 'author', align: 'center', width: '60', sortable: false},
|
{name: 'statusCode', align: 'center', width: '30', formatter: apiStatusFormatter, sortable: false},
|
||||||
|
{name: 'author', align: 'center', width: '40', sortable: false},
|
||||||
{name : 'simYn', align : 'center' , width:'40', hidden: true },
|
{name : 'simYn', align : 'center' , width:'40', hidden: true },
|
||||||
{name : 'lastModifiedDate', align : 'center' , width:'40', sortable: true},
|
{name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true},
|
||||||
{name: 'syncAsyncType', align: 'center', width: '40', hidden: true},
|
{name: 'syncAsyncType', align: 'center', width: '40', hidden: true},
|
||||||
],
|
],
|
||||||
jsonReader: {
|
jsonReader: {
|
||||||
|
|||||||
@@ -78,12 +78,30 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* iframe 모달(showModal) 콘텐츠 패딩 제거 — iframe 이 다이얼로그 폭을 최대한 사용해 내부 가로스크롤 방지 */
|
||||||
|
.ui-dialog .ui-dialog-content { padding: 0 !important; }
|
||||||
|
|
||||||
|
/* OpenAPI Spec 버튼 상태 색상 (PTL_API_SPEC_INFO 기준) */
|
||||||
|
#btn_openapi_spec.spec-exists { background:#ffe08a !important; border-color:#f0c040 !important; color:#5a4600 !important; }
|
||||||
|
#btn_openapi_spec.spec-public { background:#b8e6a0 !important; border-color:#7cc257 !important; color:#2c4a15 !important; }
|
||||||
|
/* 호버 상태 안내 레이어 */
|
||||||
|
#specStatusLayer {
|
||||||
|
position: fixed; z-index: 100000; display: none;
|
||||||
|
background: rgba(30,30,30,0.92); color: #fff;
|
||||||
|
padding: 8px 12px; border-radius: 6px;
|
||||||
|
font-size: 12px; line-height: 1.6; max-width: 340px;
|
||||||
|
white-space: pre-line; pointer-events: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script language="javascript" >
|
<script language="javascript" >
|
||||||
var isDetail = false;
|
var isDetail = false;
|
||||||
var url ='<c:url value="/onl/transaction/apim/apiInterfaceMan.json" />';
|
var url ='<c:url value="/onl/transaction/apim/apiInterfaceMan.json" />';
|
||||||
const url_excel ='<c:url value="/onl/transaction/apim/apiInterfaceMan.excel" />';
|
const url_excel ='<c:url value="/onl/transaction/apim/apiInterfaceMan.excel" />';
|
||||||
var url_spec = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
|
var url_spec = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
|
||||||
|
var url_openapi_spec = '<c:url value="/onl/transaction/apim/djbApiSpecMan.view"/>';
|
||||||
|
var url_openapi_spec_json = '<c:url value="/onl/transaction/apim/djbApiSpecMan.json"/>';
|
||||||
var returnUrl ;
|
var returnUrl ;
|
||||||
let headerValues = [];
|
let headerValues = [];
|
||||||
let headerNames = [];
|
let headerNames = [];
|
||||||
@@ -536,6 +554,8 @@
|
|||||||
|
|
||||||
$('#inboundHttpMethod').trigger('change');
|
$('#inboundHttpMethod').trigger('change');
|
||||||
$('#outboundHttpMethod').trigger('change');
|
$('#outboundHttpMethod').trigger('change');
|
||||||
|
|
||||||
|
applyOpenApiSpecStatus(); // OpenAPI Spec 버튼 색상(PTL_API_SPEC_INFO)
|
||||||
},
|
},
|
||||||
error:function(e){
|
error:function(e){
|
||||||
alert(e.responseText);
|
alert(e.responseText);
|
||||||
@@ -993,6 +1013,64 @@
|
|||||||
showModal(popupUrl, args, 1200, 800);
|
showModal(popupUrl, args, 1200, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v2: OpenAPI Spec — 기본은 모달(iframe), Shift+클릭은 새 창.
|
||||||
|
// 컨텍스트는 전부 query string 으로 전달(dialogArguments 미사용).
|
||||||
|
function showOpenApiSpecPopup(e) {
|
||||||
|
var eaiSvcName = getFullSvcName();
|
||||||
|
var eaiSvcDesc = $('#eaiSvcDesc').val();
|
||||||
|
var apipath = $('#apiFullPath').val(); // METHOD|URL ('|' 포함 → 인코딩 필수)
|
||||||
|
var contentType = $('#contentType').val();
|
||||||
|
var baseUrl = url_openapi_spec
|
||||||
|
+ '?cmd=DETAIL'
|
||||||
|
+ '&eaiSvcName=' + encodeURIComponent(eaiSvcName)
|
||||||
|
+ '&apipath=' + encodeURIComponent(apipath)
|
||||||
|
+ '&eaiSvcDesc=' + encodeURIComponent(eaiSvcDesc)
|
||||||
|
+ '&contentType=' + encodeURIComponent(contentType);
|
||||||
|
if (e && e.shiftKey) {
|
||||||
|
// Shift+클릭: 새 창. window.open 은 serviceType/menuId 자동 부착 안 되므로 직접 붙임.
|
||||||
|
window.open(urlAddServiceType(baseUrl + '&menuId=' + encodeURIComponent(getMenuId())), '_blank');
|
||||||
|
} else {
|
||||||
|
// 기본: 모달 + iframe. showModal 이 serviceType/menuId/pop 을 자동 부착.
|
||||||
|
// 콘텐츠 고정폭 1600 + 다이얼로그 chrome(패딩/보더 ~40px) 때문에 iframe 내부폭이 1600 미만이면 가로스크롤 발생.
|
||||||
|
// 뷰포트 내에서 최대한 크게 열어(캡 1720) iframe 내부폭이 1600 이상 확보되게 한다.
|
||||||
|
var vw = $(window).width(), vh = $(window).height();
|
||||||
|
var modalW = Math.min(vw - 12, 1720);
|
||||||
|
var modalH = Math.min(vh - 20, 900);
|
||||||
|
// 스펙 편집 모달 닫힘 시 버튼 색상 재조회(공개여부/등록여부 변경 반영)
|
||||||
|
showModal(baseUrl, { title: 'OpenAPI Spec' }, modalW, modalH, function(){ applyOpenApiSpecStatus(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAPI Spec 버튼 색상 = PTL_API_SPEC_INFO(api_id = getFullSvcName()) 상태.
|
||||||
|
// - 미등록: 기본색 - 등록·비공개(display_yn≠Y): 노랑 - 등록·공개(display_yn=Y): 하늘
|
||||||
|
function applyOpenApiSpecStatus() {
|
||||||
|
var $btn = $('#btn_openapi_spec');
|
||||||
|
$btn.removeClass('spec-exists spec-public').data('specMsg', '');
|
||||||
|
var eaiSvcName = getFullSvcName();
|
||||||
|
if (!eaiSvcName || eaiSvcName.length < 2) return;
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url_openapi_spec_json,
|
||||||
|
dataType: 'json',
|
||||||
|
data: { cmd: 'SPEC_STATUS', eaiSvcName: eaiSvcName },
|
||||||
|
success: function(res) {
|
||||||
|
var exists = res && (res.exists === true || res.exists === 'true');
|
||||||
|
var isPublic = res && (res.displayYn === 'Y');
|
||||||
|
var msg;
|
||||||
|
if (!exists) {
|
||||||
|
msg = 'OpenAPI Spec 미등록\n버튼을 눌러 스펙을 생성/편집하세요.';
|
||||||
|
} else if (isPublic) {
|
||||||
|
$btn.addClass('spec-public');
|
||||||
|
msg = 'OpenAPI Spec 등록됨 · 포탈 공개중 (display_yn = Y)\n포탈 메인/문서에 노출됩니다.';
|
||||||
|
} else {
|
||||||
|
$btn.addClass('spec-exists');
|
||||||
|
msg = 'OpenAPI Spec 등록됨 · 비공개 (display_yn ≠ Y)\n포탈에는 노출되지 않습니다.';
|
||||||
|
}
|
||||||
|
$btn.data('specMsg', msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
makeValidate();
|
makeValidate();
|
||||||
// var bootstrapButton = $.fn.button.noConflict()
|
// var bootstrapButton = $.fn.button.noConflict()
|
||||||
@@ -1007,9 +1085,31 @@
|
|||||||
}
|
}
|
||||||
init(url,key,detail);
|
init(url,key,detail);
|
||||||
|
|
||||||
|
// v1: 기존 API 스펙 (모달) — 기능 비교용 복원
|
||||||
$("#btn_api_spec").click(function() {
|
$("#btn_api_spec").click(function() {
|
||||||
showApiSpecPopup();
|
showApiSpecPopup();
|
||||||
});
|
});
|
||||||
|
// v2: 신규 OpenAPI Spec (새 탭)
|
||||||
|
$("#btn_openapi_spec").click(function(e) {
|
||||||
|
showOpenApiSpecPopup(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// OpenAPI Spec 버튼 호버 시 현재 상태 안내 레이어
|
||||||
|
if ($('#specStatusLayer').length === 0) {
|
||||||
|
$('body').append('<div id="specStatusLayer"></div>');
|
||||||
|
}
|
||||||
|
$("#btn_openapi_spec")
|
||||||
|
.on('mouseenter', function() {
|
||||||
|
var msg = $(this).data('specMsg');
|
||||||
|
if (!msg) return;
|
||||||
|
$('#specStatusLayer').text(msg).css('display', 'block');
|
||||||
|
})
|
||||||
|
.on('mousemove', function(e) {
|
||||||
|
$('#specStatusLayer').css({ left: (e.clientX + 14) + 'px', top: (e.clientY + 16) + 'px' });
|
||||||
|
})
|
||||||
|
.on('mouseleave', function() {
|
||||||
|
$('#specStatusLayer').hide();
|
||||||
|
});
|
||||||
|
|
||||||
$("#btn_modify").click(modifyApi);
|
$("#btn_modify").click(modifyApi);
|
||||||
$("#btn_delete").click(function(){
|
$("#btn_delete").click(function(){
|
||||||
@@ -1508,7 +1608,10 @@
|
|||||||
<button type="button" class="cssbtn" id="btn_json_export" level="R" status="DETAIL,NEW"><i class="material-icons">download</i> <%=localeMessage.getString("button.exportJson")%></button></button>
|
<button type="button" class="cssbtn" id="btn_json_export" level="R" status="DETAIL,NEW"><i class="material-icons">download</i> <%=localeMessage.getString("button.exportJson")%></button></button>
|
||||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
|
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
|
||||||
<button type="button" class="cssbtn" id="btn_modify" level="R" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
|
<button type="button" class="cssbtn" id="btn_modify" level="R" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
|
||||||
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL,NEW"><i class="material-icons">description</i> API 스펙</button>
|
<!-- v1: 기존 API 스펙 (모달) — 기능 비교용 복원 -->
|
||||||
|
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL"><i class="material-icons">description</i> API 스펙(삭제예정)</button>
|
||||||
|
<!-- v2: 신규 OpenAPI Spec (새 탭) -->
|
||||||
|
<button type="button" class="cssbtn" id="btn_openapi_spec" level="R" status="DETAIL"><i class="material-icons">api</i> OpenAPI Spec</button>
|
||||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="title" id="title" style="font-size:1.4em">${rmsMenuName}</div>
|
<div class="title" id="title" style="font-size:1.4em">${rmsMenuName}</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<script language="javascript">
|
<script language="javascript">
|
||||||
var url = '<c:url value="/onl/transaction/apim/apiSpecMan.json" />';
|
var url = '<c:url value="/onl/transaction/apim/apiSpecMan.json" />';
|
||||||
var url_org ='<c:url value="/onl/transaction/apim/apiSpecManPopup.view" />';
|
var url_org ='<c:url value="/onl/transaction/apim/apiSpecMan.view" />';
|
||||||
var isDetail = false;
|
var isDetail = false;
|
||||||
var sampleRequestEditor, sampleResponseEditor, testbedSpecEditor;
|
var sampleRequestEditor, sampleResponseEditor, testbedSpecEditor;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||||
|
<%@ page import="java.io.*"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||||
|
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||||
|
<%
|
||||||
|
response.setHeader("Pragma", "No-cache");
|
||||||
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
%>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>API 그룹 선택</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<jsp:include page="/jsp/common/include/css_custom.jsp"/>
|
||||||
|
<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/apim/apigroup/apiGroupMan.json" />';
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||||
|
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
|
var selectedGroupIds = urlParams.get("selectedGroupIds");
|
||||||
|
|
||||||
|
if (selectedGroupIds) {
|
||||||
|
selectedGroupIds = selectedGroupIds.split(','); // 콤마로 구분된 ID들을 배열로 변환
|
||||||
|
} else {
|
||||||
|
selectedGroupIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#grid').jqGrid({
|
||||||
|
datatype: "json",
|
||||||
|
mtype: 'POST',
|
||||||
|
url: url,
|
||||||
|
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||||
|
colNames: ['API 그룹 ID', 'API 그룹 명', '설명'],
|
||||||
|
colModel: [
|
||||||
|
{name: 'id', hidden: true},
|
||||||
|
{name: 'groupName', align: 'center', width: "180"},
|
||||||
|
{name: 'groupDesc', align: 'left', width: "200"}
|
||||||
|
],
|
||||||
|
jsonReader: {
|
||||||
|
repeatitems: false
|
||||||
|
},
|
||||||
|
pager: $('#pager'),
|
||||||
|
page: '${param.page}',
|
||||||
|
rowNum: '${rmsDefaultRowNum}',
|
||||||
|
autoheight: true,
|
||||||
|
height: 'auto',
|
||||||
|
width: 410,
|
||||||
|
autowidth: false,
|
||||||
|
viewrecords: true,
|
||||||
|
shrinkToFit: false,
|
||||||
|
multiselect: true,
|
||||||
|
multiboxonly: false,
|
||||||
|
loadComplete: function(data) {
|
||||||
|
var $grid = $(this);
|
||||||
|
$.each($grid.getDataIDs(), function(_, id) {
|
||||||
|
var rowData = $grid.getRowData(id);
|
||||||
|
if ($.inArray(rowData.id, selectedGroupIds) !== -1) {
|
||||||
|
$grid.jqGrid('setSelection', id, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btn_search").click(search);
|
||||||
|
|
||||||
|
$("#btn_save").click(function() {
|
||||||
|
var selectedRows = $('#grid').getGridParam('selarrrow');
|
||||||
|
|
||||||
|
var selectedGroups = [];
|
||||||
|
if (selectedRows.length > 0) {
|
||||||
|
selectedGroups = selectedRows.map(function(rowid) {
|
||||||
|
var rowData = $('#grid').getRowData(rowid);
|
||||||
|
return {
|
||||||
|
id: rowData.id,
|
||||||
|
groupName: rowData.groupName
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 부모 창의 selectApiGroup 함수 호출
|
||||||
|
if (window.parent && typeof window.parent.selectApiGroup === "function") {
|
||||||
|
window.parent.selectApiGroup(selectedGroups);
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btn_close").click(function () {
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 검색어 입력 후 엔터 키 처리
|
||||||
|
$("input[name=searchGroupName]").keydown(function(key) {
|
||||||
|
if (key.keyCode == 13) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="popup_box">
|
||||||
|
<div class="search_wrap">
|
||||||
|
<button type="button" class="cssbtn" id="btn_save" level="W">
|
||||||
|
<i class="material-icons">save</i> <%= localeMessage.getString("button.save") %>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_search" level="R">
|
||||||
|
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_close" level="R" status="DETAIL"><i
|
||||||
|
class="material-icons">close</i> <%= localeMessage.getString("button.close") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="title"></div>
|
||||||
|
<table class="search_condition" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<th style="width:80px;">API 그룹 명</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchGroupName" value="${param.searchGroupName}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table id="grid"></table>
|
||||||
|
<div id="pager"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
|
<%
|
||||||
|
response.setHeader("Pragma", "No-cache");
|
||||||
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
%>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=1600" />
|
||||||
|
<title>OpenAPI Spec 에디터</title>
|
||||||
|
|
||||||
|
<%-- Tailwind (벤더된 Play CDN JS, 오프라인) --%>
|
||||||
|
<script src="<c:url value='/plugins/openapi/tailwind.js'/>"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = { theme: { extend: { colors: { brand: {
|
||||||
|
50:'#eff6ff', 100:'#dbeafe', 500:'#3b82f6', 600:'#2563eb', 700:'#1d4ed8'
|
||||||
|
} } } } };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<%-- Swagger UI 5 (벤더) --%>
|
||||||
|
<link rel="stylesheet" href="<c:url value='/plugins/swaggerUI/swagger-ui.css'/>" />
|
||||||
|
<script src="<c:url value='/plugins/swaggerUI/swagger-ui-bundle.js'/>"></script>
|
||||||
|
<script src="<c:url value='/plugins/swaggerUI/swagger-ui-standalone-preset.js'/>"></script>
|
||||||
|
<script src="<c:url value='/plugins/swaggerUI/djb-swagger-i18n.js'/>" charset="UTF-8"></script>
|
||||||
|
|
||||||
|
<%-- js-yaml (벤더) --%>
|
||||||
|
<script src="<c:url value='/plugins/openapi/js-yaml.min.js'/>"></script>
|
||||||
|
|
||||||
|
<%-- CodeMirror (YAML 편집기 — Monaco 대체, admin addon) --%>
|
||||||
|
<link rel="stylesheet" href="<c:url value='/addon/codemirror/lib/codemirror.css'/>" />
|
||||||
|
<script src="<c:url value='/addon/codemirror/lib/codemirror.js'/>"></script>
|
||||||
|
<script src="<c:url value='/addon/codemirror/mode/yaml/yaml.js'/>"></script>
|
||||||
|
<script src="<c:url value='/addon/codemirror/mode/javascript/javascript.js'/>"></script>
|
||||||
|
|
||||||
|
<%-- jQuery + Summernote(lite, 무-Bootstrap) — 설명 리치 에디터 --%>
|
||||||
|
<link rel="stylesheet" href="<c:url value='/addon/summernote/summernote-lite.css'/>" />
|
||||||
|
<%-- 개발자포탈과 동일한 에디터 콘텐츠 스타일(.editor-content, Noto Sans KR 16px) --%>
|
||||||
|
<link rel="stylesheet" href="<c:url value='/js/djb/apispec/editor-content.css'/>" />
|
||||||
|
<script src="<c:url value='/js/jquery-1.12.1.min.js'/>"></script>
|
||||||
|
<script src="<c:url value='/addon/summernote/summernote-lite.min.js'/>"></script>
|
||||||
|
<script src="<c:url value='/addon/summernote/lang/summernote-ko-KR.js'/>"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="<c:url value='/js/djb/apispec/styles.css'/>" />
|
||||||
|
<style>
|
||||||
|
/* 페이지 자체 세로 스크롤 제거 — 뷰포트 고정, 내부 패널만 스크롤 */
|
||||||
|
html, body { height: 100%; margin: 0; overflow-x: auto; overflow-y: hidden; }
|
||||||
|
#djb-wrap { height: 100vh; display: flex; flex-direction: column; }
|
||||||
|
#djb-wrap > header, #djb-wrap > nav { flex: 0 0 auto; }
|
||||||
|
#body { flex: 1 1 auto; min-height: 0 !important; overflow: hidden; }
|
||||||
|
#form-panel { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||||
|
#form-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
||||||
|
#preview-panel { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||||
|
#preview-swagger, #preview-monaco { flex: 1 1 auto; min-height: 0; height: auto !important; overflow: auto; }
|
||||||
|
#preview-monaco .CodeMirror { height: 100%; }
|
||||||
|
/* 패널 내부 sticky 는 flex 행으로 (스크롤 컨테이너는 form-content / preview) */
|
||||||
|
#form-panel > .sticky, #preview-panel > .sticky { position: static !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.DJB_CTX = {
|
||||||
|
eaiSvcName: "${param.eaiSvcName}",
|
||||||
|
apiName: "${param.eaiSvcDesc}",
|
||||||
|
detailUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>?cmd=DETAIL_SPEC&serviceType=APIGW",
|
||||||
|
saveUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>?cmd=SAVE&serviceType=APIGW",
|
||||||
|
swaggerBase: "<c:url value='/plugins/swaggerUI/'/>",
|
||||||
|
gwAddress: "${gwAddress}",
|
||||||
|
useProxy: ${useProxy},
|
||||||
|
tokenUseProxy: ${tokenUseProxy},
|
||||||
|
exportEnabled: ${exportEnabled},
|
||||||
|
orgPopupUrl: "<c:url value='/onl/transaction/apim/apiSpecMan.view'/>",
|
||||||
|
groupPopupUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.view'/>?cmd=GROUP_POPUP",
|
||||||
|
jsonUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-50 text-slate-800 antialiased" style="min-width:1600px;">
|
||||||
|
|
||||||
|
<div id="djb-wrap" class="mx-auto" style="width:1600px;">
|
||||||
|
|
||||||
|
<!-- ===== Header ===== -->
|
||||||
|
<header class="h-14 flex items-center justify-between px-6 bg-white border-b border-slate-200 sticky top-0 z-30">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="font-semibold text-slate-900">OpenAPI Spec 에디터</div>
|
||||||
|
<span class="text-slate-300">|</span>
|
||||||
|
<span class="text-sm text-slate-500">인터페이스:</span>
|
||||||
|
<span class="text-sm font-medium text-slate-800" id="hdr-project-name">${param.eaiSvcName}</span>
|
||||||
|
<span class="ml-2 px-2 py-0.5 text-[11px] rounded-full bg-slate-100 text-slate-600 border border-slate-200" id="hdr-version">v1.0.0</span>
|
||||||
|
<span id="hdr-source" class="hidden ml-1 px-2 py-0.5 text-[11px] rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button id="btn-newwin" class="hidden px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">새창으로 띄우기</button>
|
||||||
|
<button id="btn-regen" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">자동 생성(초기화)</button>
|
||||||
|
<button id="btn-save" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700">저장</button>
|
||||||
|
<%-- 내보내기 버튼은 PTL_PROPERTY(djb.openapi.export.enabled=true) 일 때만 노출. 기본 미노출. --%>
|
||||||
|
<c:if test="${exportEnabled}">
|
||||||
|
<div class="relative">
|
||||||
|
<button id="btn-export" class="px-3 py-1.5 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">내보내기 ▾</button>
|
||||||
|
<div id="export-menu" class="hidden absolute right-0 mt-1 w-44 bg-white border border-slate-200 rounded shadow-lg z-40">
|
||||||
|
<button data-export="yaml" class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-50">YAML 다운로드</button>
|
||||||
|
<button data-export="json" class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-50">JSON 다운로드</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
|
<button id="btn-close" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">닫기</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ===== Stepper ===== -->
|
||||||
|
<nav class="bg-white border-b border-slate-200 sticky z-20" style="top:56px;">
|
||||||
|
<div class="h-1 bg-slate-100">
|
||||||
|
<div id="stepper-progress" class="h-1 bg-brand-500 transition-all duration-300" style="width:16.66%"></div>
|
||||||
|
</div>
|
||||||
|
<ol id="stepper" class="grid grid-cols-5 px-2"></ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- ===== Body ===== -->
|
||||||
|
<main id="body" class="flex" style="min-height: calc(100vh - 56px - 72px);">
|
||||||
|
<section id="form-panel" class="bg-white border-r border-slate-200 transition-all duration-300" style="width:960px;">
|
||||||
|
<div class="flex items-center justify-between px-6 py-3 border-b border-slate-100 sticky bg-white z-10" style="top:128px;">
|
||||||
|
<div class="flex items-baseline gap-3">
|
||||||
|
<span class="text-xs uppercase tracking-wide text-slate-400">Step <span id="form-step-num">1</span> / 5</span>
|
||||||
|
<h2 class="text-base font-semibold text-slate-800" id="form-step-title">기본정보</h2>
|
||||||
|
</div>
|
||||||
|
<button id="btn-toggle-preview-from-form" class="hidden text-xs px-2 py-1 rounded border border-slate-200 text-slate-600 hover:bg-slate-50">▶ 미리보기 다시 열기</button>
|
||||||
|
</div>
|
||||||
|
<div id="form-content" class="px-6 py-5"></div>
|
||||||
|
<div class="sticky bottom-0 bg-white border-t border-slate-200 px-6 py-3 flex items-center justify-between">
|
||||||
|
<button id="btn-prev" class="px-4 py-2 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed">◀ 이전</button>
|
||||||
|
<div class="text-xs text-slate-500" id="footer-hint">필수 항목을 채우고 다음 단계로 이동하세요</div>
|
||||||
|
<button id="btn-next" class="px-4 py-2 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">다음 ▶</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside id="preview-panel" class="bg-slate-50 transition-all duration-300" style="width:640px;">
|
||||||
|
<div class="flex items-center justify-between px-4 py-2 border-b border-slate-200 bg-white sticky z-10" style="top:128px;">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button data-ptab="swagger" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-brand-600 text-brand-700 font-medium">Swagger UI</button>
|
||||||
|
<button data-ptab="yaml-ro" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-transparent text-slate-600 hover:text-slate-800">YAML 보기</button>
|
||||||
|
<button data-ptab="yaml-edit" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-transparent text-slate-600 hover:text-slate-800">스펙 편집</button>
|
||||||
|
</div>
|
||||||
|
<button id="btn-toggle-preview" class="text-xs px-2 py-1 rounded border border-slate-200 text-slate-600 hover:bg-slate-100">패널 숨기기 ▶</button>
|
||||||
|
</div>
|
||||||
|
<div id="yaml-edit-bar" class="hidden flex items-center justify-between bg-amber-50 border-b border-amber-200 px-4 py-2 text-xs">
|
||||||
|
<span class="text-amber-800">YAML 을 직접 편집한 뒤 [Form 에 적용] 을 눌러 양식에 동기화하세요.</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button id="btn-yaml-revert" class="px-2 py-1 rounded border border-amber-300 bg-white hover:bg-amber-100">변경 취소</button>
|
||||||
|
<button id="btn-yaml-apply" class="px-2 py-1 rounded bg-amber-600 text-white hover:bg-amber-700">Form 에 적용</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="preview-swagger" class="overflow-auto" style="height: calc(100vh - 56px - 72px - 41px);"></div>
|
||||||
|
<div id="preview-monaco" class="hidden" style="height: calc(100vh - 56px - 72px - 41px);"></div>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast" class="fixed bottom-5 left-1/2 -translate-x-1/2 hidden px-4 py-2 rounded bg-slate-900 text-white text-sm shadow-lg z-50"></div>
|
||||||
|
|
||||||
|
<!-- Security scheme modal -->
|
||||||
|
<div id="sec-modal" class="hidden fixed inset-0 z-50 bg-slate-900/40 flex items-center justify-center">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-[640px] max-h-[80vh] overflow-auto">
|
||||||
|
<div class="flex items-center justify-between px-5 py-3 border-b border-slate-200">
|
||||||
|
<h3 class="font-semibold text-slate-800">보안 스킴 추가</h3>
|
||||||
|
<button class="text-slate-400 hover:text-slate-700" data-sec-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-5" id="sec-modal-body"></div>
|
||||||
|
<div class="px-5 py-3 border-t border-slate-200 flex justify-end gap-2 bg-slate-50">
|
||||||
|
<button class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white" data-sec-close>취소</button>
|
||||||
|
<button id="btn-sec-save" class="px-3 py-1.5 text-sm rounded bg-brand-600 text-white">추가</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="<c:url value='/js/djb/apispec/sample-data.js'/>"></script>
|
||||||
|
<script src="<c:url value='/js/djb/apispec/app.js'/>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -380,15 +380,15 @@
|
|||||||
{name: 'TRACKASISKEY2CTNT', hidden: true},
|
{name: 'TRACKASISKEY2CTNT', hidden: true},
|
||||||
|
|
||||||
{name: 'EAISVCSERNO_TMP', align: 'left', width: '220', formatter: trimFormatter},
|
{name: 'EAISVCSERNO_TMP', align: 'left', width: '220', formatter: trimFormatter},
|
||||||
{name: 'KEYMGTMSGCTNT', align: 'left', width: '240'},
|
{name: 'KEYMGTMSGCTNT', align: 'left', width: '300'},
|
||||||
{name: 'LOGPRCSSSERNO_TMP', align: 'center', width: '40'},
|
{name: 'LOGPRCSSSERNO_TMP', align: 'center', width: '40'},
|
||||||
{name: 'TELGMRECVTRANCD', align: 'left', width: '135', hidden: true},
|
{name: 'TELGMRECVTRANCD', align: 'left', width: '135', hidden: true},
|
||||||
{name: 'EAISVCNAME', align: 'left', width: '130'},
|
{name: 'EAISVCNAME', align: 'left', width: '160'},
|
||||||
{name: 'EAISVCDESC', align: 'left', width: '280'},
|
{name: 'EAISVCDESC', align: 'left', width: '280'},
|
||||||
{name: 'EAIBZWKDSTCD_TMP', align: 'center', width: '50'},
|
{name: 'EAIBZWKDSTCD_TMP', align: 'center', width: '50'},
|
||||||
{name: 'EXTBIZCD', align: 'center', width: '50', hidden: isApiGw},
|
{name: 'EXTBIZCD', align: 'center', width: '50', hidden: isApiGw},
|
||||||
{name: 'REFKEY', align: 'left', width: '280', hidden: isApiGw},
|
{name: 'REFKEY', align: 'left', width: '280', hidden: isApiGw},
|
||||||
{name: 'CLIENTNAME', align: 'left', width: '260', hidden: !isApiGw},
|
{name: 'CLIENTNAME', align: 'left', width: '120', hidden: !isApiGw},
|
||||||
{name: 'ORGNAME', align: 'left', width: '100', hidden: !isApiGw, formatter: orgNameFormatter},
|
{name: 'ORGNAME', align: 'left', width: '100', hidden: !isApiGw, formatter: orgNameFormatter},
|
||||||
{name: 'MSGDPSTYMS_TMP', align: 'center', width: '150', formatter: timeStampFormat2},
|
{name: 'MSGDPSTYMS_TMP', align: 'center', width: '150', formatter: timeStampFormat2},
|
||||||
{name: 'MSGPRCSSYMS', align: 'center', width: '150', formatter: timeStampFormat2},
|
{name: 'MSGPRCSSYMS', align: 'center', width: '150', formatter: timeStampFormat2},
|
||||||
|
|||||||
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,122 @@
|
|||||||
|
/*!
|
||||||
|
* djb-swagger-i18n.js
|
||||||
|
* Swagger UI 영문 라벨을 한글로 치환하는 i18n 패치 (방법 B - DOM patch)
|
||||||
|
* 배포 경로 권장: static/plugins/swaggerUI/djb-swagger-i18n.js
|
||||||
|
* index.html 의 swagger-initializer.js 직전 또는 직후에 <script> 로 로드
|
||||||
|
*
|
||||||
|
* ?lang=en 쿼리스트링이 있으면 패치 비활성화 (원문 보기)
|
||||||
|
* 매핑 카탈로그는 01-i18n-mapping/i18n-strings.csv 와 동기 유지
|
||||||
|
*/
|
||||||
|
(function (window) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ── 비활성화 옵션 ──
|
||||||
|
if (/[?&]lang=en\b/.test(window.location.search)) return;
|
||||||
|
|
||||||
|
// ── 매핑 (key 는 디버깅용, 실제 비교는 EN 문자열 일치) ──
|
||||||
|
var I18N_MAP = {
|
||||||
|
// P0 — OAuth2 모달 본문
|
||||||
|
'auth.modal.title': { en: 'Available authorizations', ko: '사용 가능한 인증' },
|
||||||
|
'auth.scopes.description': {
|
||||||
|
en: 'Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.',
|
||||||
|
ko: '스코프는 최종 사용자를 대신하여 애플리케이션이 데이터에 접근할 수 있는 권한 수준을 부여합니다. 각 API는 하나 이상의 스코프를 선언할 수 있습니다.'
|
||||||
|
},
|
||||||
|
'auth.scopes.swagger_grant': {
|
||||||
|
en: 'API requires the following scopes. Select which ones you want to grant to Swagger UI.',
|
||||||
|
ko: '이 API 는 아래 스코프가 필요합니다. Swagger UI 에 부여할 스코프를 선택하세요.'
|
||||||
|
},
|
||||||
|
'auth.flow.clientCredentials': { en: 'OAuth2 client credentials flow', ko: 'OAuth2 클라이언트 자격증명 흐름' },
|
||||||
|
'auth.flow.implicit': { en: 'OAuth2 implicit flow', ko: 'OAuth2 암묵적 흐름' },
|
||||||
|
'auth.flow.password': { en: 'OAuth2 password flow', ko: 'OAuth2 비밀번호 흐름' },
|
||||||
|
'auth.flow.authorizationCode': { en: 'OAuth2 authorization code flow', ko: 'OAuth2 인가 코드 흐름' },
|
||||||
|
'auth.field.tokenUrl': { en: 'Token URL:', ko: '토큰 URL:' },
|
||||||
|
'auth.field.flow': { en: 'Flow:', ko: '흐름:' },
|
||||||
|
'auth.field.clientId': { en: 'client_id:', ko: '클라이언트 ID (client_id):' },
|
||||||
|
'auth.field.clientSecret': { en: 'client_secret:',ko: '클라이언트 시크릿 (client_secret):' },
|
||||||
|
'auth.field.scopes': { en: 'Scopes:', ko: '스코프:' },
|
||||||
|
'auth.action.selectAll': { en: 'select all', ko: '모두 선택' },
|
||||||
|
'auth.action.selectNone': { en: 'select none', ko: '선택 해제' },
|
||||||
|
'auth.action.authorize': { en: 'Authorize', ko: '인증' },
|
||||||
|
'auth.action.close': { en: 'Close', ko: '닫기' },
|
||||||
|
'auth.action.logout': { en: 'Logout', ko: '로그아웃' },
|
||||||
|
'auth.modal.authorized': { en: 'authorized', ko: '인증됨' },
|
||||||
|
|
||||||
|
// P1 — 기타 인증 흐름
|
||||||
|
'auth.scheme.apiKey': { en: 'API key', ko: 'API 키' },
|
||||||
|
'auth.field.name': { en: 'name:', ko: '이름:' },
|
||||||
|
'auth.field.in': { en: 'in:', ko: '위치:' },
|
||||||
|
'auth.field.value': { en: 'Value:', ko: '값:' },
|
||||||
|
'basic.field.username': { en: 'Username:',ko: '사용자 ID:' },
|
||||||
|
'basic.field.password': { en: 'Password:',ko: '비밀번호:' }
|
||||||
|
// P2 (오퍼레이션/응답) 는 필요 시 추가
|
||||||
|
};
|
||||||
|
|
||||||
|
// 빠른 lookup 을 위해 EN → KO 인덱스 사전 생성
|
||||||
|
var EN_TO_KO = Object.create(null);
|
||||||
|
Object.keys(I18N_MAP).forEach(function (k) {
|
||||||
|
var e = I18N_MAP[k];
|
||||||
|
EN_TO_KO[e.en] = e.ko;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 치환 대상 노드 식별 + 치환 ──
|
||||||
|
function translateNode(node) {
|
||||||
|
if (!node) return;
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
var raw = node.nodeValue;
|
||||||
|
if (!raw) return;
|
||||||
|
var trimmed = raw.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
// 1) 완전 일치 (가장 안전)
|
||||||
|
if (EN_TO_KO[trimmed]) {
|
||||||
|
node.nodeValue = raw.replace(trimmed, EN_TO_KO[trimmed]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2) 라벨 colon 패턴: "Token URL:" 같이 끝이 ":" 인 짧은 라벨
|
||||||
|
// (불필요 — 1번 완전 일치로 대부분 커버)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||||
|
if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE') return;
|
||||||
|
|
||||||
|
// input placeholder / button title 도 일부 케이스에 대비
|
||||||
|
if (node.tagName === 'INPUT' && node.placeholder && EN_TO_KO[node.placeholder.trim()]) {
|
||||||
|
node.placeholder = EN_TO_KO[node.placeholder.trim()];
|
||||||
|
}
|
||||||
|
|
||||||
|
var i, child = node.childNodes, len = child.length;
|
||||||
|
for (i = 0; i < len; i++) translateNode(child[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateAll() {
|
||||||
|
var root = document.getElementById('swagger-ui') || document.body;
|
||||||
|
translateNode(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MutationObserver — SwaggerUI 가 동적으로 DOM 을 다시 그릴 때마다 재적용 ──
|
||||||
|
var observer = new MutationObserver(function (mutations) {
|
||||||
|
for (var i = 0; i < mutations.length; i++) {
|
||||||
|
var m = mutations[i];
|
||||||
|
if (m.addedNodes && m.addedNodes.length) {
|
||||||
|
for (var j = 0; j < m.addedNodes.length; j++) translateNode(m.addedNodes[j]);
|
||||||
|
}
|
||||||
|
if (m.type === 'characterData') {
|
||||||
|
translateNode(m.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
translateAll();
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', start);
|
||||||
|
} else {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
})(window);
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+15
-11
@@ -4,7 +4,7 @@ plugins {
|
|||||||
id 'eclipse-wtp'
|
id 'eclipse-wtp'
|
||||||
id 'idea'
|
id 'idea'
|
||||||
id 'war'
|
id 'war'
|
||||||
id 'com.diffplug.eclipse.apt' version '3.41.1'
|
id 'org.cyclonedx.bom' version '3.2.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
group 'com.eactive'
|
group 'com.eactive'
|
||||||
@@ -49,10 +49,6 @@ compileJava {
|
|||||||
options.encoding = 'UTF-8'
|
options.encoding = 'UTF-8'
|
||||||
options.compilerArgs = ['-parameters']
|
options.compilerArgs = ['-parameters']
|
||||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||||
|
|
||||||
aptOptions {
|
|
||||||
processorArgs = [ 'querydsl.generatedAnnotationClass' : 'com.querydsl.core.annotations.Generated' ]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
war {
|
war {
|
||||||
@@ -89,7 +85,7 @@ dependencies {
|
|||||||
implementation project(':elink-online-emsclient')
|
implementation project(':elink-online-emsclient')
|
||||||
implementation project(':elink-portal-common')
|
implementation project(':elink-portal-common')
|
||||||
//implementation project(':kjb-safedb')
|
//implementation project(':kjb-safedb')
|
||||||
implementation project(":eapim-admin-djb")
|
//implementation project(":eapim-admin-djb")
|
||||||
|
|
||||||
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
|
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||||
@@ -122,6 +118,12 @@ dependencies {
|
|||||||
//implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
//implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
|
||||||
|
|
||||||
|
// apistatus-draft.yml (자동 탐지 초안 양식) 로딩용.
|
||||||
|
// admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없어 직접 파싱한다.
|
||||||
|
// 2.x 를 쓴다 - 1.x 는 CVE-2022-1471(Constructor 역직렬화 RCE) 및
|
||||||
|
// CVE-2022-25857/38749~38752(DoS) 영향. 로딩도 SafeConstructor 로 한다.
|
||||||
|
implementation 'org.yaml:snakeyaml:2.4'
|
||||||
|
|
||||||
implementation "javax.jms:javax.jms-api:2.0"
|
implementation "javax.jms:javax.jms-api:2.0"
|
||||||
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"
|
||||||
@@ -257,11 +259,11 @@ eclipse {
|
|||||||
}
|
}
|
||||||
synchronizationTasks settingEclipseEncoding, initDirs
|
synchronizationTasks settingEclipseEncoding, initDirs
|
||||||
jdt {
|
jdt {
|
||||||
apt {
|
// apt {
|
||||||
// generated 된 패스 경로를 .setting/org.eclipse.jdt.apt.core.prefs 의 org.eclipse.jdt.apt.genSrcDir에 적용한다.
|
// // generated 된 패스 경로를 .setting/org.eclipse.jdt.apt.core.prefs 의 org.eclipse.jdt.apt.genSrcDir에 적용한다.
|
||||||
// project > properties > Java Compiler > Annoation Processing 화면에서 확인 가능하다.
|
// // project > properties > Java Compiler > Annoation Processing 화면에서 확인 가능하다.
|
||||||
genSrcDir = file(generatedJavaDir)
|
// genSrcDir = file(generatedJavaDir)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
project {
|
project {
|
||||||
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
|
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
|
||||||
@@ -276,3 +278,5 @@ tasks.withType(JavaCompile) {
|
|||||||
options.forkOptions.memoryMaximumSize = '4g' // 컴파일러에 2GB 할당 (필요시 4g로 증량)
|
options.forkOptions.memoryMaximumSize = '4g' // 컴파일러에 2GB 할당 (필요시 4g로 증량)
|
||||||
options.encoding = 'UTF-8'
|
options.encoding = 'UTF-8'
|
||||||
}
|
}
|
||||||
|
// CycloneDX SBOM -> xlsx 변환 (gradle sbomXlsx)
|
||||||
|
apply from: "$projectDir/gradle/sbom-xlsx.gradle"
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
/*
|
||||||
|
* CycloneDX SBOM(bom.json) -> Excel(xlsx) 변환 태스크.
|
||||||
|
*
|
||||||
|
* gradle sbomXlsx # cyclonedxBom 실행 후 변환
|
||||||
|
* gradle sbomXlsx -PsbomJson=path.json # 기존 bom.json 사용(cyclonedxBom 생략)
|
||||||
|
* gradle sbomXlsx -PsbomOut=out.xlsx # 출력 경로 지정
|
||||||
|
*
|
||||||
|
* buildscript 블록이 이 스크립트에만 적용되므로 POI 의존성이 메인 빌드
|
||||||
|
* classpath 나 WAR 산출물에는 포함되지 않는다.
|
||||||
|
*
|
||||||
|
* 시트: 요약 / WAR 기준
|
||||||
|
* 산출 기준은 war 태스크의 classpath(= runtimeClasspath) 이므로
|
||||||
|
* test·annotationProcessor·developmentOnly·compileOnly 의존은 모두 제외된다.
|
||||||
|
* bom.json 은 라이선스/해시/설명/직접-전이 판별을 위한 메타 소스로만 쓴다.
|
||||||
|
*/
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
url "https://nexus.eactive.synology.me:8090/repository/maven-public/"
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'org.apache.poi:poi-ooxml:3.17'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import groovy.json.JsonSlurper
|
||||||
|
import org.apache.poi.ss.usermodel.BorderStyle
|
||||||
|
import org.apache.poi.ss.usermodel.FillPatternType
|
||||||
|
import org.apache.poi.ss.usermodel.HorizontalAlignment
|
||||||
|
import org.apache.poi.ss.usermodel.IndexedColors
|
||||||
|
import org.apache.poi.ss.usermodel.VerticalAlignment
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddress
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||||
|
|
||||||
|
// 엑셀 셀 문자열 상한(32767)보다 여유를 둔 절단 길이
|
||||||
|
ext.SBOM_CELL_LIMIT = 32000
|
||||||
|
|
||||||
|
task sbomXlsx {
|
||||||
|
group = 'sbom'
|
||||||
|
description = 'CycloneDX bom.json 을 WAR 수록 기준 xlsx 로 변환한다'
|
||||||
|
|
||||||
|
// -PsbomJson 으로 기존 산출물을 지정하면 재생성하지 않는다
|
||||||
|
if (!project.hasProperty('sbomJson')) {
|
||||||
|
dependsOn 'cyclonedxBom'
|
||||||
|
}
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
File src = resolveBomJson(project)
|
||||||
|
File out = project.hasProperty('sbomOut')
|
||||||
|
? project.file(project.property('sbomOut'))
|
||||||
|
: new File(project.buildDir, "reports/sbom/${sbomFileName(project)}")
|
||||||
|
out.parentFile.mkdirs()
|
||||||
|
|
||||||
|
def bom = new JsonSlurper().parse(src, 'UTF-8')
|
||||||
|
def deploy = collectDeployJars(project)
|
||||||
|
def warRows = joinWarRows(deploy.jars, indexComponents(bom))
|
||||||
|
|
||||||
|
def wb = new XSSFWorkbook()
|
||||||
|
def st = createStyles(wb)
|
||||||
|
writeSummarySheet(wb, st, bom, warRows, src, deploy.label)
|
||||||
|
writeWarSheet(wb, st, warRows)
|
||||||
|
|
||||||
|
out.withOutputStream { os -> wb.write(os) }
|
||||||
|
wb.close()
|
||||||
|
|
||||||
|
int unmatched = warRows.count { it.matched == 'N' }
|
||||||
|
logger.lifecycle("SBOM xlsx 생성: ${out.absolutePath} " +
|
||||||
|
"(배포 수록 ${warRows.size()}개, SBOM 미매칭 ${unmatched}개, 원본 ${src.name})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 산출 파일명: 배포 패키지명 기준 (war 있으면 war 파일명, 없으면 project 이름[-버전]) */
|
||||||
|
String sbomFileName(Project p) {
|
||||||
|
def warTask = p.tasks.findByName('war')
|
||||||
|
if (warTask != null) {
|
||||||
|
String archive = warTask.archiveFileName.get()
|
||||||
|
return archive.replaceAll(/\.(war|jar|ear)$/, '') + '-sbom.xlsx'
|
||||||
|
}
|
||||||
|
String ver = (p.version == null || p.version.toString() in ['', 'unspecified']) ? '' : "-${p.version}"
|
||||||
|
return "${p.name}${ver}-sbom.xlsx"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** bom.json 위치 결정: -PsbomJson > cyclonedxBom 산출 경로 후보 */
|
||||||
|
File resolveBomJson(Project p) {
|
||||||
|
if (p.hasProperty('sbomJson')) {
|
||||||
|
File f = p.file(p.property('sbomJson'))
|
||||||
|
if (!f.exists()) {
|
||||||
|
throw new GradleException("bom.json 없음: ${f.absolutePath}")
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
def candidates = [
|
||||||
|
new File(p.buildDir, 'reports/cyclonedx/bom.json'),
|
||||||
|
new File(p.buildDir, 'reports/bom.json'),
|
||||||
|
]
|
||||||
|
File found = candidates.find { it.exists() }
|
||||||
|
if (found == null) {
|
||||||
|
throw new GradleException(
|
||||||
|
"bom.json 을 찾지 못했다. 확인한 경로: " + candidates*.absolutePath.join(', ') +
|
||||||
|
"\n'gradle cyclonedxBom' 실행 후 재시도하거나 -PsbomJson=<경로> 로 지정한다.")
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실제 배포물에 packaging 되는 jar 목록.
|
||||||
|
* war 프로젝트는 war 태스크 classpath(= runtimeClasspath), 그 외는 runtimeClasspath 를
|
||||||
|
* 기준으로 하므로 test/annotationProcessor/developmentOnly/compileOnly 는 자동으로 빠진다.
|
||||||
|
*
|
||||||
|
* @return [label: 기준 설명, jars: 행 목록]
|
||||||
|
*/
|
||||||
|
Map collectDeployJars(Project p) {
|
||||||
|
def cfg = p.configurations.findByName('runtimeClasspath')
|
||||||
|
if (cfg == null) {
|
||||||
|
p.logger.warn("[${p.name}] runtimeClasspath 가 없어 배포 기준 시트를 비운다")
|
||||||
|
return [label: '(없음)', jars: []]
|
||||||
|
}
|
||||||
|
|
||||||
|
def warTask = p.tasks.findByName('war')
|
||||||
|
def files
|
||||||
|
String label
|
||||||
|
if (warTask != null) {
|
||||||
|
files = warTask.classpath.files
|
||||||
|
label = 'WAR WEB-INF/lib (war 태스크 classpath)'
|
||||||
|
} else {
|
||||||
|
files = cfg.files
|
||||||
|
label = 'runtimeClasspath (war 태스크 없음)'
|
||||||
|
}
|
||||||
|
|
||||||
|
def coordByFile = [:]
|
||||||
|
cfg.resolvedConfiguration.resolvedArtifacts.each { a ->
|
||||||
|
def id = a.moduleVersion.id
|
||||||
|
coordByFile[a.file] = [group: id.group, name: id.name, version: id.version]
|
||||||
|
}
|
||||||
|
def jars = files.findAll { it.name.endsWith('.jar') }.collect { f ->
|
||||||
|
def c = coordByFile[f]
|
||||||
|
[
|
||||||
|
file : f.name,
|
||||||
|
group : c?.group ?: '',
|
||||||
|
name : c?.name ?: f.name.replaceAll(/\.jar$/, ''),
|
||||||
|
version: c?.version ?: '',
|
||||||
|
coord : c ? "${c.group}:${c.name}:${c.version}".toString() : '',
|
||||||
|
]
|
||||||
|
}.sort { it.file }
|
||||||
|
|
||||||
|
return [label: label, jars: jars]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** bom.json 컴포넌트를 'group:name:version' 키로 색인 (라이선스/해시/설명/직접-전이) */
|
||||||
|
Map indexComponents(bom) {
|
||||||
|
String rootRef = bom.metadata?.component?.'bom-ref'
|
||||||
|
Set directRefs = (bom.dependencies?.find { it.ref == rootRef }?.dependsOn ?: []) as Set
|
||||||
|
|
||||||
|
def index = [:]
|
||||||
|
bom.components?.each { c ->
|
||||||
|
def hashes = [:]
|
||||||
|
c.hashes?.each { h -> hashes[h.alg] = h.content }
|
||||||
|
|
||||||
|
def licenses = (c.licenses ?: []).collect { l ->
|
||||||
|
l.license?.id ?: l.license?.name ?: l.expression ?: ''
|
||||||
|
}.findAll { it }
|
||||||
|
|
||||||
|
index["${c.group ?: ''}:${c.name ?: ''}:${c.version ?: ''}".toString()] = [
|
||||||
|
direct : directRefs.contains(c.'bom-ref') ? '직접' : '전이',
|
||||||
|
licenses : licenses.join('; '),
|
||||||
|
licenseList: licenses.isEmpty() ? ['(미상)'] : licenses,
|
||||||
|
purl : c.purl ?: '',
|
||||||
|
sha256 : hashes['SHA-256'] ?: '',
|
||||||
|
sha1 : hashes['SHA-1'] ?: '',
|
||||||
|
description: c.description ?: '',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WAR jar 목록에 SBOM 메타를 좌표로 결합 */
|
||||||
|
List joinWarRows(List warJars, Map index) {
|
||||||
|
def result = []
|
||||||
|
warJars.eachWithIndex { j, i ->
|
||||||
|
def m = j.coord ? index[j.coord] : null
|
||||||
|
result << [
|
||||||
|
no : i + 1,
|
||||||
|
file : j.file,
|
||||||
|
group : j.group,
|
||||||
|
name : j.name,
|
||||||
|
version : j.version,
|
||||||
|
direct : m?.direct ?: '',
|
||||||
|
licenses : m?.licenses ?: '',
|
||||||
|
licenseList: m?.licenseList ?: ['(미상)'],
|
||||||
|
purl : m?.purl ?: '',
|
||||||
|
sha256 : m?.sha256 ?: '',
|
||||||
|
sha1 : m?.sha1 ?: '',
|
||||||
|
matched : (m != null) ? 'Y' : 'N',
|
||||||
|
description: m?.description ?: '',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
Map createStyles(wb) {
|
||||||
|
def headFont = wb.createFont()
|
||||||
|
headFont.setBold(true)
|
||||||
|
headFont.setColor(IndexedColors.WHITE.getIndex())
|
||||||
|
|
||||||
|
def head = wb.createCellStyle()
|
||||||
|
head.setFont(headFont)
|
||||||
|
head.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex())
|
||||||
|
head.setFillPattern(FillPatternType.SOLID_FOREGROUND)
|
||||||
|
head.setAlignment(HorizontalAlignment.CENTER)
|
||||||
|
head.setVerticalAlignment(VerticalAlignment.CENTER)
|
||||||
|
head.setBorderBottom(BorderStyle.THIN)
|
||||||
|
|
||||||
|
def body = wb.createCellStyle()
|
||||||
|
body.setVerticalAlignment(VerticalAlignment.TOP)
|
||||||
|
|
||||||
|
def wrap = wb.createCellStyle()
|
||||||
|
wrap.setVerticalAlignment(VerticalAlignment.TOP)
|
||||||
|
wrap.setWrapText(true)
|
||||||
|
|
||||||
|
def labelFont = wb.createFont()
|
||||||
|
labelFont.setBold(true)
|
||||||
|
def label = wb.createCellStyle()
|
||||||
|
label.setFont(labelFont)
|
||||||
|
|
||||||
|
return [head: head, body: body, wrap: wrap, label: label]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 헤더 행 생성 + 폭 지정 + 틀고정 */
|
||||||
|
def writeHeader(sheet, style, List<String> headers, List<Integer> widths) {
|
||||||
|
def row = sheet.createRow(0)
|
||||||
|
row.setHeightInPoints(20f)
|
||||||
|
headers.eachWithIndex { h, i ->
|
||||||
|
def cell = row.createCell(i)
|
||||||
|
cell.setCellValue(h)
|
||||||
|
cell.setCellStyle(style)
|
||||||
|
sheet.setColumnWidth(i, widths[i] * 256)
|
||||||
|
}
|
||||||
|
sheet.createFreezePane(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
def cellOf(row, int idx, value, style) {
|
||||||
|
def cell = row.createCell(idx)
|
||||||
|
String s = (value == null) ? '' : value.toString()
|
||||||
|
if (s.length() > SBOM_CELL_LIMIT) {
|
||||||
|
s = s.substring(0, SBOM_CELL_LIMIT) + '…(생략)'
|
||||||
|
}
|
||||||
|
cell.setCellValue(s)
|
||||||
|
cell.setCellStyle(style)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
def writeSummarySheet(wb, st, bom, List warRows, File src, String basisLabel) {
|
||||||
|
def sheet = wb.createSheet('요약')
|
||||||
|
def comp = bom.metadata?.component ?: [:]
|
||||||
|
def tool = bom.metadata?.tools?.components?.getAt(0)
|
||||||
|
Set licenseKinds = warRows.collectMany { it.licenseList } as Set
|
||||||
|
|
||||||
|
def items = [
|
||||||
|
['대상 프로젝트', "${comp.group ?: ''}:${comp.name ?: ''}:${comp.version ?: ''}"],
|
||||||
|
['산출 기준', "${basisLabel} — test/annotationProcessor/compileOnly 제외"],
|
||||||
|
['BOM 포맷', "${bom.bomFormat ?: ''} ${bom.specVersion ?: ''}"],
|
||||||
|
['serialNumber', bom.serialNumber ?: ''],
|
||||||
|
['생성 시각', bom.metadata?.timestamp ?: ''],
|
||||||
|
['생성 도구', tool ? "${tool.name} ${tool.version}" : ''],
|
||||||
|
['원본 파일', src.absolutePath],
|
||||||
|
['배포 수록 jar', warRows.size()],
|
||||||
|
[' └ 직접 의존', warRows.count { it.direct == '직접' }],
|
||||||
|
[' └ 전이 의존', warRows.count { it.direct == '전이' }],
|
||||||
|
[' └ SBOM 미매칭', warRows.count { it.matched == 'N' }],
|
||||||
|
['라이선스 종류', licenseKinds.size()],
|
||||||
|
['라이선스 미상', warRows.count { it.licenses.isEmpty() }],
|
||||||
|
]
|
||||||
|
|
||||||
|
writeHeader(sheet, st.head, ['항목', '값'], [30, 90])
|
||||||
|
items.eachWithIndex { item, i ->
|
||||||
|
def row = sheet.createRow(i + 1)
|
||||||
|
cellOf(row, 0, item[0], st.label)
|
||||||
|
cellOf(row, 1, item[1], st.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이선스 분포 (요약 하단)
|
||||||
|
def byLicense = [:].withDefault { 0 }
|
||||||
|
warRows.each { r -> r.licenseList.each { lic -> byLicense[lic] = byLicense[lic] + 1 } }
|
||||||
|
def sorted = byLicense.entrySet().sort { a, b -> (b.value <=> a.value) ?: (a.key <=> b.key) }
|
||||||
|
|
||||||
|
int base = items.size() + 2
|
||||||
|
def hdr = sheet.createRow(base)
|
||||||
|
cellOf(hdr, 0, '라이선스', st.head)
|
||||||
|
cellOf(hdr, 1, 'jar 수', st.head)
|
||||||
|
sorted.eachWithIndex { e, i ->
|
||||||
|
def row = sheet.createRow(base + 1 + i)
|
||||||
|
cellOf(row, 0, e.key, st.body)
|
||||||
|
cellOf(row, 1, e.value, st.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 실제 배포물(WAR WEB-INF/lib) 기준 시트 */
|
||||||
|
def writeWarSheet(wb, st, List warRows) {
|
||||||
|
def sheet = wb.createSheet('WAR 기준')
|
||||||
|
def headers = ['No', 'jar 파일명', 'Group', 'Name', 'Version', '구분',
|
||||||
|
'License', 'purl', 'SHA-256', 'SHA-1', 'SBOM매칭', 'Description']
|
||||||
|
def widths = [6, 46, 32, 34, 16, 7, 30, 60, 40, 30, 10, 60]
|
||||||
|
writeHeader(sheet, st.head, headers, widths)
|
||||||
|
|
||||||
|
warRows.eachWithIndex { r, i ->
|
||||||
|
def row = sheet.createRow(i + 1)
|
||||||
|
cellOf(row, 0, r.no, st.body)
|
||||||
|
cellOf(row, 1, r.file, st.body)
|
||||||
|
cellOf(row, 2, r.group, st.body)
|
||||||
|
cellOf(row, 3, r.name, st.body)
|
||||||
|
cellOf(row, 4, r.version, st.body)
|
||||||
|
cellOf(row, 5, r.direct, st.body)
|
||||||
|
cellOf(row, 6, r.licenses, st.body)
|
||||||
|
cellOf(row, 7, r.purl, st.body)
|
||||||
|
cellOf(row, 8, r.sha256, st.body)
|
||||||
|
cellOf(row, 9, r.sha1, st.body)
|
||||||
|
cellOf(row, 10, r.matched, st.body)
|
||||||
|
cellOf(row, 11, r.description, st.wrap)
|
||||||
|
}
|
||||||
|
if (!warRows.isEmpty()) {
|
||||||
|
sheet.setAutoFilter(new CellRangeAddress(0, warRows.size(), 0, headers.size() - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -7,7 +7,7 @@ include 'elink-online-common'
|
|||||||
include 'elink-online-emsclient'
|
include 'elink-online-emsclient'
|
||||||
include 'elink-portal-common'
|
include 'elink-portal-common'
|
||||||
//include 'kjb-safedb'
|
//include 'kjb-safedb'
|
||||||
include 'eapim-admin-djb'
|
//include 'eapim-admin-djb'
|
||||||
|
|
||||||
project (':elink-online-core-jpa').projectDir = new File(settingsDir, "../eapim-online/elink-online-core-jpa")
|
project (':elink-online-core-jpa').projectDir = new File(settingsDir, "../eapim-online/elink-online-core-jpa")
|
||||||
project (':elink-online-core').projectDir = new File(settingsDir, "../eapim-online/elink-online-core")
|
project (':elink-online-core').projectDir = new File(settingsDir, "../eapim-online/elink-online-core")
|
||||||
@@ -16,4 +16,4 @@ project (':elink-online-common').projectDir = new File(settingsDir, "../eapim-on
|
|||||||
project (':elink-online-emsclient').projectDir = new File(settingsDir, "../eapim-online/elink-online-emsclient")
|
project (':elink-online-emsclient').projectDir = new File(settingsDir, "../eapim-online/elink-online-emsclient")
|
||||||
project (':elink-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
|
project (':elink-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
|
||||||
//project (':kjb-safedb').projectDir = new File(settingsDir, "../kjb-safedb")
|
//project (':kjb-safedb').projectDir = new File(settingsDir, "../kjb-safedb")
|
||||||
project (':eapim-admin-djb').projectDir = new File(settingsDir, "../eapim-admin-djb")
|
//project (':eapim-admin-djb').projectDir = new File(settingsDir, "../eapim-admin-djb")
|
||||||
+3
@@ -51,6 +51,9 @@ public class AuditLogPageRepositoryImpl implements AuditLogPageRepository {
|
|||||||
if (StringUtils.isNotBlank(command.getSearchRemoteAddress())) {
|
if (StringUtils.isNotBlank(command.getSearchRemoteAddress())) {
|
||||||
jpaQuery.where(qAuditLogEntity.remoteAddress.containsIgnoreCase(command.getSearchRemoteAddress()));
|
jpaQuery.where(qAuditLogEntity.remoteAddress.containsIgnoreCase(command.getSearchRemoteAddress()));
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isNotBlank(command.getSearchParameters())) {
|
||||||
|
jpaQuery.where(qAuditLogEntity.parameters.containsIgnoreCase(command.getSearchParameters()));
|
||||||
|
}
|
||||||
|
|
||||||
long totalCount = jpaQuery.fetchOne();
|
long totalCount = jpaQuery.fetchOne();
|
||||||
List<AuditLogEntity> auditLogs = jpaQuery
|
List<AuditLogEntity> auditLogs = jpaQuery
|
||||||
|
|||||||
+1
@@ -18,6 +18,7 @@ public class SelectListAuditLogCommand {
|
|||||||
|
|
||||||
private String searchUserId;
|
private String searchUserId;
|
||||||
private String searchRemoteAddress;
|
private String searchRemoteAddress;
|
||||||
|
private String searchParameters;
|
||||||
|
|
||||||
@JsonFormat(pattern = "yyyyMMddHHmmss")
|
@JsonFormat(pattern = "yyyyMMddHHmmss")
|
||||||
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
|
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.eactive.eai.rms.common.acl.sitemap;
|
package com.eactive.eai.rms.common.acl.sitemap;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
@@ -7,6 +11,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import com.eactive.eai.rms.common.base.BaseAnnotationController;
|
import com.eactive.eai.rms.common.base.BaseAnnotationController;
|
||||||
|
import com.eactive.eai.rms.common.login.SessionManager;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -15,8 +20,10 @@ public class SitemapController extends BaseAnnotationController {
|
|||||||
private final SitemapService sitemapService;
|
private final SitemapService sitemapService;
|
||||||
|
|
||||||
@GetMapping(value = "/common/acl/sitemap/sitemapMan.view")
|
@GetMapping(value = "/common/acl/sitemap/sitemapMan.view")
|
||||||
public String view(@RequestParam(value = "serviceType", required = false) String serviceType, Model model) {
|
public String view(@RequestParam(value = "serviceType", required = false) String serviceType,
|
||||||
model.addAttribute("sitemapTree", sitemapService.getSitemapTree(serviceType));
|
HttpServletRequest request, Model model) {
|
||||||
|
List<String> roleIds = SessionManager.getRoleId(request);
|
||||||
|
model.addAttribute("sitemapTree", sitemapService.getSitemapTree(roleIds, serviceType));
|
||||||
return "/common/acl/sitemap/sitemapMan";
|
return "/common/acl/sitemap/sitemapMan";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ import com.eactive.eai.rms.common.acl.sitemap.ui.SitemapNode;
|
|||||||
|
|
||||||
public interface SitemapService {
|
public interface SitemapService {
|
||||||
|
|
||||||
public List<SitemapNode> getSitemapTree(String serviceType);
|
public List<SitemapNode> getSitemapTree(List<String> roleIds, String serviceType);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class SitemapServiceImpl extends BaseService implements SitemapService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RoleMenuAuthService roleMenuAuthService;
|
private RoleMenuAuthService roleMenuAuthService;
|
||||||
|
|
||||||
public List<SitemapNode> getSitemapTree(String serviceType) {
|
public List<SitemapNode> getSitemapTree(List<String> roleIds, String serviceType) {
|
||||||
Menu root = menuService.getById("ROOT_DEV");
|
Menu root = menuService.getById("ROOT_DEV");
|
||||||
Menu langRoot = findLangRoot(root);
|
Menu langRoot = findLangRoot(root);
|
||||||
List<SitemapNode> tree = new ArrayList<>();
|
List<SitemapNode> tree = new ArrayList<>();
|
||||||
@@ -38,7 +38,7 @@ public class SitemapServiceImpl extends BaseService implements SitemapService {
|
|||||||
|
|
||||||
Set<String> serviceTypeMenuIds = StringUtils.isBlank(serviceType)
|
Set<String> serviceTypeMenuIds = StringUtils.isBlank(serviceType)
|
||||||
? null
|
? null
|
||||||
: new HashSet<>(roleMenuAuthService.findMenuIdsByServiceType(serviceType));
|
: new HashSet<>(roleMenuAuthService.findMenuIdsByRoleIdsAndServiceType(roleIds, serviceType));
|
||||||
|
|
||||||
for (Menu menu : sortedChildren(langRoot)) {
|
for (Menu menu : sortedChildren(langRoot)) {
|
||||||
if (isVisible(menu) && matchesServiceType(menu, serviceTypeMenuIds)) {
|
if (isVisible(menu) && matchesServiceType(menu, serviceTypeMenuIds)) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserManService extends BaseService {
|
public class UserManService extends BaseService {
|
||||||
|
|
||||||
|
private static final String USER_STATUS_NORMAL = "1";
|
||||||
|
|
||||||
private final UserInfoService userInfoService;
|
private final UserInfoService userInfoService;
|
||||||
private final UserRoleService userRoleService;
|
private final UserRoleService userRoleService;
|
||||||
@@ -100,6 +100,7 @@ public class UserManService extends BaseService {
|
|||||||
|
|
||||||
UserInfo userInfo = userUIMapper.toEntity(userUI);
|
UserInfo userInfo = userUIMapper.toEntity(userUI);
|
||||||
userInfo.setRoleidnfiname(CommonConstants.DEPT_DEVELOPER);
|
userInfo.setRoleidnfiname(CommonConstants.DEPT_DEVELOPER);
|
||||||
|
userInfo.setStatus(USER_STATUS_NORMAL);
|
||||||
|
|
||||||
String subfix = monitoringContext.getStringProperty(MonitoringContext.RMS_PASSWORD_INIT_SUBFIX, "@!");
|
String subfix = monitoringContext.getStringProperty(MonitoringContext.RMS_PASSWORD_INIT_SUBFIX, "@!");
|
||||||
|
|
||||||
@@ -133,6 +134,12 @@ public class UserManService extends BaseService {
|
|||||||
public void update(UserUI userUI) {
|
public void update(UserUI userUI) {
|
||||||
UserInfo userInfo = userInfoService.getById(userUI.getUserId());
|
UserInfo userInfo = userInfoService.getById(userUI.getUserId());
|
||||||
userUIMapper.updateToEntity(userUI, userInfo);
|
userUIMapper.updateToEntity(userUI, userInfo);
|
||||||
|
|
||||||
|
// 계정상태를 정상으로 저장하면 로그인 실패횟수 초기화
|
||||||
|
if (USER_STATUS_NORMAL.equals(userInfo.getStatus())) {
|
||||||
|
userInfo.setLoginfailcount(0);
|
||||||
|
}
|
||||||
|
|
||||||
userInfoService.save(userInfo);
|
userInfoService.save(userInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +155,11 @@ public class UserManService extends BaseService {
|
|||||||
UserInfo userInfo = userInfoService.getById(userUI.getUserId());
|
UserInfo userInfo = userInfoService.getById(userUI.getUserId());
|
||||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256, userUI.getUserId()+subfix));
|
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256, userUI.getUserId()+subfix));
|
||||||
userUIMapper.updateToEntity(userUI, userInfo);
|
userUIMapper.updateToEntity(userUI, userInfo);
|
||||||
|
|
||||||
|
// 비밀번호 초기화 시 계정상태를 정상으로 변경하고 로그인 실패횟수 초기화
|
||||||
|
userInfo.setLoginfailcount(0);
|
||||||
|
userInfo.setStatus(USER_STATUS_NORMAL);
|
||||||
|
|
||||||
userInfoService.save(userInfo);
|
userInfoService.save(userInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public interface UserUIMapper extends GenericMapper<UserUI, UserInfo> {
|
|||||||
@Mapping(source = "secondmentstdt", target = "SECONDMENTSTDT")
|
@Mapping(source = "secondmentstdt", target = "SECONDMENTSTDT")
|
||||||
@Mapping(source = "secondmentendt", target = "SECONDMENTENDT")
|
@Mapping(source = "secondmentendt", target = "SECONDMENTENDT")
|
||||||
@Mapping(source = "allowip", target = "allowIp")
|
@Mapping(source = "allowip", target = "allowIp")
|
||||||
|
@Mapping(source = "lastloginyms", target = "LASTLOGINYMS")
|
||||||
|
@Mapping(source = "regdyms", target = "REGDYMS")
|
||||||
UserUI toVo(UserInfo entity);
|
UserUI toVo(UserInfo entity);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -82,4 +82,14 @@ public class UserUI {
|
|||||||
|
|
||||||
@JsonProperty("STATUS")
|
@JsonProperty("STATUS")
|
||||||
private String status; // 사용자계정상태
|
private String status; // 사용자계정상태
|
||||||
|
|
||||||
|
@JsonProperty("LASTLOGINYMS")
|
||||||
|
@JsonFormat(pattern = LocalDateTimeFormatters.FORMAT_YYYYMMDDHHMMSS_14)
|
||||||
|
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
|
||||||
|
private LocalDateTime LASTLOGINYMS; // 최종로그인일시
|
||||||
|
|
||||||
|
@JsonProperty("REGDYMS")
|
||||||
|
@JsonFormat(pattern = LocalDateTimeFormatters.FORMAT_YYYYMMDDHHMMSS_14)
|
||||||
|
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
|
||||||
|
private LocalDateTime REGDYMS; // 등록일시
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ public interface MonitoringContext {
|
|||||||
// 이중 로그인 허용여부
|
// 이중 로그인 허용여부
|
||||||
public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED";
|
public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED";
|
||||||
|
|
||||||
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 10)
|
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 60)
|
||||||
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
|
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
|
||||||
|
|
||||||
// 웹훅
|
// 웹훅
|
||||||
@@ -266,6 +266,9 @@ public interface MonitoringContext {
|
|||||||
//비밀번호 초기화 접미사
|
//비밀번호 초기화 접미사
|
||||||
public static final String RMS_PASSWORD_INIT_SUBFIX = "rms.password.init.subfix";
|
public static final String RMS_PASSWORD_INIT_SUBFIX = "rms.password.init.subfix";
|
||||||
|
|
||||||
|
// 최대 로그인 실패 허용횟수 (0이면 계정 잠금 미적용)
|
||||||
|
public static final String RMS_PASSWORD_FAIL_COUNT = "rms.password.fail.count";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import com.eactive.eai.rms.common.util.ContainerUtil;
|
|||||||
import com.eactive.eai.rms.data.entity.man.monitoringProperty.MonitoringPropertyId;
|
import com.eactive.eai.rms.data.entity.man.monitoringProperty.MonitoringPropertyId;
|
||||||
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyGroupService;
|
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyGroupService;
|
||||||
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
|
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
|
||||||
import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
import com.eactive.eai.rms.ext.djb.common.DjbPropertyHolder;
|
||||||
import com.eactive.ext.kjb.util.KjbPropertyInjector;
|
import com.eactive.eai.rms.ext.djb.common.DjbPropertyInjector;
|
||||||
|
|
||||||
@Service("monitoringContext")
|
@Service("monitoringContext")
|
||||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||||
@@ -36,7 +36,7 @@ class MonitoringContextImpl implements MonitoringContext {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private MonitoringContextDAO monitoringContextDAO;
|
private MonitoringContextDAO monitoringContextDAO;
|
||||||
|
|
||||||
private Properties properties = null;
|
private Properties properties = new Properties();
|
||||||
|
|
||||||
private String instancePropertyGroupName;
|
private String instancePropertyGroupName;
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ class MonitoringContextImpl implements MonitoringContext {
|
|||||||
|
|
||||||
public void refresh() {
|
public void refresh() {
|
||||||
init();
|
init();
|
||||||
KjbPropertyHolder.reload();
|
DjbPropertyHolder.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refresh(String propName) {
|
public void refresh(String propName) {
|
||||||
@@ -73,7 +73,10 @@ class MonitoringContextImpl implements MonitoringContext {
|
|||||||
String hostIp = InetAddress.getLocalHost().getHostAddress();
|
String hostIp = InetAddress.getLocalHost().getHostAddress();
|
||||||
this.instancePropertyGroupName = String
|
this.instancePropertyGroupName = String
|
||||||
.format(MonitoringPropertyGroupService.MONITORING_PROPERTY_KEY_FORMAT, hostIp);
|
.format(MonitoringPropertyGroupService.MONITORING_PROPERTY_KEY_FORMAT, hostIp);
|
||||||
this.properties = monitoringContextDAO.getProperties(instancePropertyGroupName);
|
Properties loaded = monitoringContextDAO.getProperties(instancePropertyGroupName);
|
||||||
|
if (loaded != null) {
|
||||||
|
this.properties = loaded;
|
||||||
|
}
|
||||||
|
|
||||||
String eaiAgentId = System.getProperty(EAI_AGENTID);
|
String eaiAgentId = System.getProperty(EAI_AGENTID);
|
||||||
if (StringUtils.isBlank(eaiAgentId)) {
|
if (StringUtils.isBlank(eaiAgentId)) {
|
||||||
@@ -85,9 +88,9 @@ class MonitoringContextImpl implements MonitoringContext {
|
|||||||
|
|
||||||
setPortMapper(monitoringContextDAO.getPortMap());
|
setPortMapper(monitoringContextDAO.getPortMap());
|
||||||
|
|
||||||
// KjbPropertyHolder 초기화
|
// DjbPropertyHolder 초기화
|
||||||
KjbPropertyInjector injector = new KjbPropertyInjector(monitoringPropertyService, "Monitoring");
|
DjbPropertyInjector injector = new DjbPropertyInjector(monitoringPropertyService, "Monitoring");
|
||||||
KjbPropertyHolder.initialize(injector::inject);
|
DjbPropertyHolder.initialize(injector::inject);
|
||||||
|
|
||||||
logger.info("SSO AgentId : " + System.getProperty(EAI_AGENTID));
|
logger.info("SSO AgentId : " + System.getProperty(EAI_AGENTID));
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
|||||||
import org.springframework.web.servlet.View;
|
import org.springframework.web.servlet.View;
|
||||||
|
|
||||||
import com.eactive.eai.common.seed.Seed;
|
import com.eactive.eai.common.seed.Seed;
|
||||||
import com.eactive.eai.rms.ext.kjb.smsauth.SmsAuthService;
|
|
||||||
import com.eactive.ext.djb.DamoManager;
|
import com.eactive.ext.djb.DamoManager;
|
||||||
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
|
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
|
||||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||||
@@ -51,6 +50,7 @@ import com.eactive.eai.rms.data.entity.man.user.UserLoginHistoryService;
|
|||||||
import com.eactive.eai.rms.data.entity.man.user.UserRole;
|
import com.eactive.eai.rms.data.entity.man.user.UserRole;
|
||||||
import com.eactive.eai.rms.data.entity.man.user.UserRoleService;
|
import com.eactive.eai.rms.data.entity.man.user.UserRoleService;
|
||||||
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
|
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
|
||||||
|
import com.eactive.eai.rms.ext.djb.smsauth.SmsAuthService;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
public class MainController implements InterceptorSkipController {
|
public class MainController implements InterceptorSkipController {
|
||||||
@@ -64,6 +64,9 @@ public class MainController implements InterceptorSkipController {
|
|||||||
private static final String REDIRECT_LOGIN_URL = "redirect:/emergency.jsp";
|
private static final String REDIRECT_LOGIN_URL = "redirect:/emergency.jsp";
|
||||||
private static final String REDIRECT_CHANGE_PASSWORD_URL = "redirect:/emergency.jsp";
|
private static final String REDIRECT_CHANGE_PASSWORD_URL = "redirect:/emergency.jsp";
|
||||||
|
|
||||||
|
private static final String USER_STATUS_LOCKED = "2";
|
||||||
|
private static final int DEFAULT_MAX_LOGIN_FAIL_COUNT = 5;
|
||||||
|
|
||||||
private final LocaleMessage localeMessage;
|
private final LocaleMessage localeMessage;
|
||||||
private final MonitoringContext monitoringContext;
|
private final MonitoringContext monitoringContext;
|
||||||
|
|
||||||
@@ -160,7 +163,7 @@ public class MainController implements InterceptorSkipController {
|
|||||||
session.removeAttribute(RESULT_MSG);
|
session.removeAttribute(RESULT_MSG);
|
||||||
return LoginResponseDto.builder()
|
return LoginResponseDto.builder()
|
||||||
.success(false)
|
.success(false)
|
||||||
.errorMessage(errorMsg != null ? errorMsg : "로그인에 실패했습니다.")
|
.errorMessage(toAlertMessage(errorMsg != null ? errorMsg : "로그인에 실패했습니다."))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,9 +348,19 @@ public class MainController implements InterceptorSkipController {
|
|||||||
|
|
||||||
//logger.info("사용자 계정 상태: " + userInfo.getStatus());
|
//logger.info("사용자 계정 상태: " + userInfo.getStatus());
|
||||||
|
|
||||||
if ( userInfo.getStatus() != null && "2".equals(userInfo.getStatus()) ) {
|
if ( USER_STATUS_LOCKED.equals(userInfo.getStatus()) ) {
|
||||||
|
int maxLoginFailCount = getMaxLoginFailCount();
|
||||||
|
boolean lockedByFailCount = maxLoginFailCount > 0
|
||||||
|
&& userInfo.getLoginfailcount() != null
|
||||||
|
&& userInfo.getLoginfailcount() >= maxLoginFailCount;
|
||||||
|
|
||||||
|
if (lockedByFailCount) {
|
||||||
|
setLoginFailure(request, session, "login.accountlocked1",
|
||||||
|
"login.accountlocked2", String.valueOf(maxLoginFailCount));
|
||||||
|
} else {
|
||||||
setLoginFailure(request, session, "login.checkstatus1",
|
setLoginFailure(request, session, "login.checkstatus1",
|
||||||
"login.checkstatus2");
|
"login.checkstatus2");
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,8 +387,9 @@ public class MainController implements InterceptorSkipController {
|
|||||||
/* 기존로직 - LDAP 사용 x */
|
/* 기존로직 - LDAP 사용 x */
|
||||||
if (!userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256, dto.getPassword()))) {
|
if (!userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256, dto.getPassword()))) {
|
||||||
// password 다름.
|
// password 다름.
|
||||||
|
int failCount = increaseLoginFailCount(userInfo);
|
||||||
setLoginFailure(request, session, "login.loginfail1",
|
setLoginFailure(request, session, "login.loginfail1",
|
||||||
"login.loginfail2");
|
"login.loginfail2", failCount);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -389,8 +403,9 @@ public class MainController implements InterceptorSkipController {
|
|||||||
//if (!authenticate(userInfo.getUserid(), dto.getPassword())) {
|
//if (!authenticate(userInfo.getUserid(), dto.getPassword())) {
|
||||||
if (!authenticate(userInfo.getUserid(), password)) {
|
if (!authenticate(userInfo.getUserid(), password)) {
|
||||||
// password 다름.
|
// password 다름.
|
||||||
|
int failCount = increaseLoginFailCount(userInfo);
|
||||||
setLoginFailure(request, session, "login.loginfail1",
|
setLoginFailure(request, session, "login.loginfail1",
|
||||||
"login.loginfail2");
|
"login.loginfail2", failCount);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,6 +447,27 @@ public class MainController implements InterceptorSkipController {
|
|||||||
return userInfo;
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 최대 로그인 실패 허용횟수 조회 (rms.password.fail.count, 0이면 계정 잠금 미적용)
|
||||||
|
private int getMaxLoginFailCount() {
|
||||||
|
return monitoringContext.getIntProperty(
|
||||||
|
MonitoringContext.RMS_PASSWORD_FAIL_COUNT, DEFAULT_MAX_LOGIN_FAIL_COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금
|
||||||
|
private int increaseLoginFailCount(UserInfo userInfo) {
|
||||||
|
int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1;
|
||||||
|
userInfo.setLoginfailcount(failCount);
|
||||||
|
|
||||||
|
int maxLoginFailCount = getMaxLoginFailCount();
|
||||||
|
if (maxLoginFailCount > 0 && failCount >= maxLoginFailCount) {
|
||||||
|
userInfo.setStatus(USER_STATUS_LOCKED);
|
||||||
|
logger.warn("로그인 " + maxLoginFailCount + "회 이상 실패로 계정 잠금 처리: " + userInfo.getUserid());
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfoService.save(userInfo);
|
||||||
|
return failCount;
|
||||||
|
}
|
||||||
|
|
||||||
// LDAP
|
// LDAP
|
||||||
public boolean authenticate(String userId, String password) {
|
public boolean authenticate(String userId, String password) {
|
||||||
String LDAP_URL = monitoringPropertyService.getPrpty2ValById("Monitoring", "ldap.domain");
|
String LDAP_URL = monitoringPropertyService.getPrpty2ValById("Monitoring", "ldap.domain");
|
||||||
@@ -496,6 +532,7 @@ public class MainController implements InterceptorSkipController {
|
|||||||
String lastLoginIp = IpUtil.getClientIp(request);
|
String lastLoginIp = IpUtil.getClientIp(request);
|
||||||
userInfo.setLastloginyms(LocalDateTime.now());
|
userInfo.setLastloginyms(LocalDateTime.now());
|
||||||
userInfo.setLastloginip(lastLoginIp);
|
userInfo.setLastloginip(lastLoginIp);
|
||||||
|
userInfo.setLoginfailcount(0);
|
||||||
userInfoService.save(userInfo);
|
userInfoService.save(userInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,12 +593,32 @@ public class MainController implements InterceptorSkipController {
|
|||||||
return "redirect:/common/errors/errorLogon.jsp";
|
return "redirect:/common/errors/errorLogon.jsp";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTML 개행 태그(<BR/> 등)를 JS alert용 개행문자로 변환
|
||||||
|
private String toAlertMessage(String message) {
|
||||||
|
return message.replaceAll("(?i)<br\\s*/?>", "\n");
|
||||||
|
}
|
||||||
|
|
||||||
// 로그인 실패 시 로그 기록, 세션에 실패 메시지 설정
|
// 로그인 실패 시 로그 기록, 세션에 실패 메시지 설정
|
||||||
private void setLoginFailure(HttpServletRequest request,
|
private void setLoginFailure(HttpServletRequest request,
|
||||||
HttpSession session, String logKey, String msgKey) {
|
HttpSession session, String logKey, String msgKey,
|
||||||
|
String... msgArgs) {
|
||||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
|
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
|
||||||
localeMessage.getString(logKey));
|
localeMessage.getString(logKey));
|
||||||
session.setAttribute(RESULT_MSG, localeMessage.getString(msgKey));
|
session.setAttribute(RESULT_MSG, localeMessage.getString(msgKey, msgArgs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인 실패 시 로그 기록, 세션에 실패 메시지(실패 횟수 포함) 설정
|
||||||
|
private void setLoginFailure(HttpServletRequest request,
|
||||||
|
HttpSession session, String logKey, String msgKey,
|
||||||
|
int failCount) {
|
||||||
|
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
|
||||||
|
localeMessage.getString(logKey));
|
||||||
|
String msg = localeMessage.getString(msgKey);
|
||||||
|
int maxLoginFailCount = getMaxLoginFailCount();
|
||||||
|
if (maxLoginFailCount > 0) {
|
||||||
|
msg += " (" + failCount + "/" + maxLoginFailCount + ")";
|
||||||
|
}
|
||||||
|
session.setAttribute(RESULT_MSG, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 서비스 타입 검증
|
// 서비스 타입 검증
|
||||||
@@ -627,7 +684,7 @@ public class MainController implements InterceptorSkipController {
|
|||||||
model.addAttribute("mainPage", mainPage);
|
model.addAttribute("mainPage", mainPage);
|
||||||
model.addAttribute("menuId", menuId);
|
model.addAttribute("menuId", menuId);
|
||||||
model.addAttribute("autoLogoutTimeout",
|
model.addAttribute("autoLogoutTimeout",
|
||||||
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 10));
|
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 60));
|
||||||
|
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
logger.debug("CURRENT SERVICE TYPE : " + service);
|
logger.debug("CURRENT SERVICE TYPE : " + service);
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@ public class MonitoringPropertyManController extends BaseAnnotationController {
|
|||||||
monitoringContext.refresh();
|
monitoringContext.refresh();
|
||||||
|
|
||||||
String url = "/monitoring/refresh.do";
|
String url = "/monitoring/refresh.do";
|
||||||
Map<String, String> ret = monitoringSyncAgent.targetSync(url, paramMap);
|
Map<String, String> ret = monitoringSyncAgent.sync(url, paramMap);
|
||||||
return ret.get("result");
|
return ret.get("result");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class RoleMenuAuthService extends AbstractEMSDataSerivce<RoleMenuAuth, Ro
|
|||||||
.fetch();
|
.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> findMenuIdsByServiceType(String serviceType) {
|
public List<String> findMenuIdsByRoleIdsAndServiceType(List<String> roleIds, String serviceType) {
|
||||||
|
|
||||||
QRoleMenuAuth qRoleMenuAuth = QRoleMenuAuth.roleMenuAuth;
|
QRoleMenuAuth qRoleMenuAuth = QRoleMenuAuth.roleMenuAuth;
|
||||||
|
|
||||||
@@ -59,7 +59,9 @@ public class RoleMenuAuthService extends AbstractEMSDataSerivce<RoleMenuAuth, Ro
|
|||||||
.select(qRoleMenuAuth.id.menuId)
|
.select(qRoleMenuAuth.id.menuId)
|
||||||
.distinct()
|
.distinct()
|
||||||
.from(qRoleMenuAuth)
|
.from(qRoleMenuAuth)
|
||||||
.where(qRoleMenuAuth.id.serviceType.eq(serviceType))
|
.where(qRoleMenuAuth.id.roleId.in(roleIds),
|
||||||
|
qRoleMenuAuth.auth.in("R", "W"),
|
||||||
|
qRoleMenuAuth.id.serviceType.eq(serviceType))
|
||||||
.setHint(QueryHints.CACHEABLE, true)
|
.setHint(QueryHints.CACHEABLE, true)
|
||||||
.fetch();
|
.fetch();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.eactive.eai.rms.data.entity.onl.apim.apigroup;
|
|||||||
|
|
||||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
||||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroupApi;
|
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroupApi;
|
||||||
|
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroupApiId;
|
||||||
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup;
|
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup;
|
||||||
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
|
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
|
||||||
import com.eactive.eai.data.jpa.AbstractDataService;
|
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||||
@@ -75,4 +76,37 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
|
|||||||
.where(qApiGroup.id.in(apiGroupIds))
|
.where(qApiGroup.id.in(apiGroupIds))
|
||||||
.fetch();
|
.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */
|
||||||
|
public void setApiGroupsForApi(String apiId, List<String> groupIds) {
|
||||||
|
deleteApiGroupApiByApiId(apiId);
|
||||||
|
if (groupIds == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String gid : groupIds) {
|
||||||
|
if (StringUtils.isBlank(gid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ApiGroup group = repository.findById(gid).orElse(null);
|
||||||
|
if (group == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Hibernate.initialize(group.getApiGroupApiList());
|
||||||
|
List<ApiGroupApi> list = group.getApiGroupApiList();
|
||||||
|
boolean exists = false;
|
||||||
|
for (ApiGroupApi a : list) {
|
||||||
|
if (a.getId() != null && apiId.equals(a.getId().getApiId())) {
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!exists) {
|
||||||
|
ApiGroupApi a = new ApiGroupApi();
|
||||||
|
a.setId(new ApiGroupApiId(gid, apiId));
|
||||||
|
a.setDisplayOrder(list.size() + 1);
|
||||||
|
list.add(a);
|
||||||
|
repository.save(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -65,6 +65,17 @@ public class MessageRequestService
|
|||||||
return repository.count(predicate);
|
return repository.count(predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 특정 일자에 EMAIL 로 발송 요청된 건수 (UMS 일련번호 채번용). */
|
||||||
|
public long countEmailRequestsOnDate(LocalDate date) {
|
||||||
|
QMessageRequest q = QMessageRequest.messageRequest;
|
||||||
|
BooleanBuilder predicate = new BooleanBuilder();
|
||||||
|
predicate.and(q.messageType.in("EMAIL"));
|
||||||
|
predicate.and(q.requestDate.goe(date.atStartOfDay()));
|
||||||
|
predicate.and(q.requestDate.loe(date.atTime(23, 59, 59)));
|
||||||
|
return repository.count(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** PENDING 상태인 건만 FAILED 로 전환한다. */
|
/** PENDING 상태인 건만 FAILED 로 전환한다. */
|
||||||
public void updateStatusToFailedIfPending(String id) {
|
public void updateStatusToFailedIfPending(String id) {
|
||||||
MessageRequest entity = getById(id);
|
MessageRequest entity = getById(id);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 판정 구간 내 오류가 1건 이상 집계된 API 의 오류율 (개발 모드 진단 로그용)
|
||||||
|
*
|
||||||
|
* <p>{@code ApiStatusRepository.findApiErrorRates} 결과. 장애 판정 임계값과 무관하게
|
||||||
|
* 오류율 0% 초과인 API 를 모두 담는다.</p>
|
||||||
|
*/
|
||||||
|
public interface ApiErrorRateRow {
|
||||||
|
|
||||||
|
/** API(EAI 서비스)명 */
|
||||||
|
String getEaisvcname();
|
||||||
|
|
||||||
|
/** 현재 상태 코드 - 정상(N), 점검(C), 지연(D), 장애(E) */
|
||||||
|
String getStatusCode();
|
||||||
|
|
||||||
|
/** 판정 구간 내 총 건수 (성공 + 타임아웃 + 시스템오류 + 업무오류) */
|
||||||
|
BigDecimal getTotal();
|
||||||
|
|
||||||
|
/** 판정 구간 내 성공 건수 */
|
||||||
|
BigDecimal getSuccess();
|
||||||
|
|
||||||
|
/** 오류율(%) - (총건수 - 성공건수) / 총건수 * 100 */
|
||||||
|
BigDecimal getErrorRate();
|
||||||
|
|
||||||
|
/** 오류 판정 구간 내 건수 가중 평균 응답시간(ms) */
|
||||||
|
BigDecimal getAvgRespTime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지연 판정 구간 내 총 건수.
|
||||||
|
*
|
||||||
|
* <p>지연은 오류와 판정 구간이 다르다({@code api.status.delay.range_minute}).
|
||||||
|
* 지연 임계 비교는 이 구간의 평균 응답시간으로 해야 실제 판정과 일치한다.</p>
|
||||||
|
*/
|
||||||
|
BigDecimal getDelayTotal();
|
||||||
|
|
||||||
|
/** 지연 판정 구간 내 건수 가중 평균 응답시간(ms) */
|
||||||
|
BigDecimal getDelayAvgRespTime();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EAI 서비스명 조회 결과 (TSEAIHE01)
|
||||||
|
*/
|
||||||
|
public interface ApiNameRow {
|
||||||
|
String getEaisvcname();
|
||||||
|
String getEaisvcdesc();
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import com.eactive.eai.data.entity.onl.message.QServiceMessageEntity;
|
|||||||
import com.eactive.eai.data.entity.onl.routing.QRouting;
|
import com.eactive.eai.data.entity.onl.routing.QRouting;
|
||||||
import com.eactive.eai.data.entity.onl.stdmessage.QStandardMessageInfo;
|
import com.eactive.eai.data.entity.onl.stdmessage.QStandardMessageInfo;
|
||||||
import com.eactive.eai.data.jpa.AbstractDataService;
|
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.QApiStatus;
|
||||||
import com.eactive.eai.rms.data.entity.onl.authserver.QScopeInfo;
|
import com.eactive.eai.rms.data.entity.onl.authserver.QScopeInfo;
|
||||||
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessService;
|
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessService;
|
||||||
import com.querydsl.core.BooleanBuilder;
|
import com.querydsl.core.BooleanBuilder;
|
||||||
@@ -45,6 +46,7 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
|||||||
QServiceMessageEntity qServiceMessageEntity = QServiceMessageEntity.serviceMessageEntity;
|
QServiceMessageEntity qServiceMessageEntity = QServiceMessageEntity.serviceMessageEntity;
|
||||||
QRouting qRouting = QRouting.routing;
|
QRouting qRouting = QRouting.routing;
|
||||||
QStandardMessageInfo qStdInfo = QStandardMessageInfo.standardMessageInfo;
|
QStandardMessageInfo qStdInfo = QStandardMessageInfo.standardMessageInfo;
|
||||||
|
QApiStatus qApiStatus = QApiStatus.apiStatus;
|
||||||
|
|
||||||
JPAQuery<Tuple> jpaQuery = createBaseQuery(eaiMessageUISearch);
|
JPAQuery<Tuple> jpaQuery = createBaseQuery(eaiMessageUISearch);
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
|||||||
, qRouting.nonmotivrouturiname
|
, qRouting.nonmotivrouturiname
|
||||||
, qStdInfo.apifullpath
|
, qStdInfo.apifullpath
|
||||||
, qStdInfo.bzwksvckeyname
|
, qStdInfo.bzwksvckeyname
|
||||||
|
, qApiStatus.statusCode
|
||||||
|
|
||||||
)
|
)
|
||||||
.limit(pageable.getPageSize())
|
.limit(pageable.getPageSize())
|
||||||
@@ -105,6 +108,7 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
|||||||
QServiceMessageEntity qServiceMessageEntity = QServiceMessageEntity.serviceMessageEntity;
|
QServiceMessageEntity qServiceMessageEntity = QServiceMessageEntity.serviceMessageEntity;
|
||||||
QRouting qRouting = QRouting.routing;
|
QRouting qRouting = QRouting.routing;
|
||||||
QStandardMessageInfo qStdInfo = QStandardMessageInfo.standardMessageInfo;
|
QStandardMessageInfo qStdInfo = QStandardMessageInfo.standardMessageInfo;
|
||||||
|
QApiStatus qApiStatus = QApiStatus.apiStatus;
|
||||||
|
|
||||||
BooleanBuilder predicate = createSelectListPredicate(eaiMessageUISearch);
|
BooleanBuilder predicate = createSelectListPredicate(eaiMessageUISearch);
|
||||||
|
|
||||||
@@ -114,6 +118,7 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
|||||||
, qRouting.nonmotivrouturiname
|
, qRouting.nonmotivrouturiname
|
||||||
, qStdInfo.apifullpath
|
, qStdInfo.apifullpath
|
||||||
, qStdInfo.bzwksvckeyname
|
, qStdInfo.bzwksvckeyname
|
||||||
|
, qApiStatus.statusCode
|
||||||
)
|
)
|
||||||
.from(qeaiMessageEntity)
|
.from(qeaiMessageEntity)
|
||||||
.join(qRouting)
|
.join(qRouting)
|
||||||
@@ -122,6 +127,8 @@ public class EAIMessageService extends AbstractDataService<EAIMessageEntity, Str
|
|||||||
.on(qServiceMessageEntity.id.svcprcssno.eq(1))
|
.on(qServiceMessageEntity.id.svcprcssno.eq(1))
|
||||||
.leftJoin(qStdInfo)
|
.leftJoin(qStdInfo)
|
||||||
.on(qeaiMessageEntity.eaisvcname.eq(qStdInfo.eaisvcname))
|
.on(qeaiMessageEntity.eaisvcname.eq(qStdInfo.eaisvcname))
|
||||||
|
.leftJoin(qApiStatus)
|
||||||
|
.on(qeaiMessageEntity.eaisvcname.eq(qApiStatus.eaisvcname))
|
||||||
.where(predicate);
|
.where(predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -1,5 +1,6 @@
|
|||||||
package com.eactive.eai.rms.data.entity.onl.inflow;
|
package com.eactive.eai.rms.data.entity.onl.inflow;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -63,11 +64,19 @@ public class InflowControlLogService
|
|||||||
|
|
||||||
public Iterable<InflowControlLog> findAll(InflowControlHistoryManUISearch uiSearch) {
|
public Iterable<InflowControlLog> findAll(InflowControlHistoryManUISearch uiSearch) {
|
||||||
QInflowControlLog qInflowConfrolLog = QInflowControlLog.inflowControlLog;
|
QInflowControlLog qInflowConfrolLog = QInflowControlLog.inflowControlLog;
|
||||||
|
String startTime = StringUtils.defaultIfEmpty(uiSearch.getSearchStartTime(), "000000");
|
||||||
|
String endTime = StringUtils.defaultIfEmpty(uiSearch.getSearchEndTime(), "235959");
|
||||||
BooleanBuilder booleanBuilder = new BooleanBuilder();
|
BooleanBuilder booleanBuilder = new BooleanBuilder();
|
||||||
booleanBuilder
|
booleanBuilder
|
||||||
.and(qInflowConfrolLog.id.msgdpstyms
|
.and(qInflowConfrolLog.id.msgdpstyms
|
||||||
.between(uiSearch.getSearchStartDate() + "000000000",
|
.between(uiSearch.getSearchStartDate() + startTime + "000",
|
||||||
uiSearch.getSearchEndDate() + "235959999"));
|
uiSearch.getSearchEndDate() + endTime + "999"));
|
||||||
|
if (!StringUtils.isEmpty(uiSearch.getSearchInstanceName()))
|
||||||
|
booleanBuilder.and(qInflowConfrolLog.eaisevrinstncname.contains(uiSearch.getSearchInstanceName()));
|
||||||
|
if (!StringUtils.isEmpty(uiSearch.getSearchApiName()))
|
||||||
|
booleanBuilder.and(qInflowConfrolLog.eaisvcname.contains(uiSearch.getSearchApiName()));
|
||||||
|
if (!StringUtils.isEmpty(uiSearch.getSearchAdapterGroupName()))
|
||||||
|
booleanBuilder.and(qInflowConfrolLog.adptrbzwkgroupname.contains(uiSearch.getSearchAdapterGroupName()));
|
||||||
return super.repository.findAll(booleanBuilder);
|
return super.repository.findAll(booleanBuilder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ 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.dsl.BooleanExpression;
|
||||||
|
import com.querydsl.core.types.dsl.StringPath;
|
||||||
import com.querydsl.jpa.impl.JPAQuery;
|
import com.querydsl.jpa.impl.JPAQuery;
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||||
|
|
||||||
@@ -46,7 +48,12 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
@Autowired
|
@Autowired
|
||||||
InflowControlManMapper mapper;
|
InflowControlManMapper mapper;
|
||||||
|
|
||||||
public Page<InflowControlServiceDto> findAllForAdapter(Pageable pageable, String searchName) {
|
// 유량제어 미설정(useyn null)은 "사용안함" 검색 시 함께 조회되도록 처리
|
||||||
|
private static BooleanExpression useYnCondition(StringPath useYnPath, String searchUseYn) {
|
||||||
|
return "0".equals(searchUseYn) ? useYnPath.eq(searchUseYn).or(useYnPath.isNull()) : useYnPath.eq(searchUseYn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<InflowControlServiceDto> findAllForAdapter(Pageable pageable, String searchName, String searchUseYn) {
|
||||||
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
|
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
|
||||||
QAdapterGroup qAdapterGroup = QAdapterGroup.adapterGroup;
|
QAdapterGroup qAdapterGroup = QAdapterGroup.adapterGroup;
|
||||||
QInflowControl qInflowControl = QInflowControl.inflowControl;
|
QInflowControl qInflowControl = QInflowControl.inflowControl;
|
||||||
@@ -54,6 +61,8 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
BooleanBuilder boolBuilder = new BooleanBuilder();
|
BooleanBuilder boolBuilder = new BooleanBuilder();
|
||||||
if (!StringUtils.isEmpty(searchName))
|
if (!StringUtils.isEmpty(searchName))
|
||||||
boolBuilder.and(qAdapterGroup.adptrbzwkgroupname.contains(searchName));
|
boolBuilder.and(qAdapterGroup.adptrbzwkgroupname.contains(searchName));
|
||||||
|
if (!StringUtils.isEmpty(searchUseYn))
|
||||||
|
boolBuilder.and(useYnCondition(qInflowControl.useyn, searchUseYn));
|
||||||
|
|
||||||
BooleanBuilder adapterWherePredicate = new BooleanBuilder(qAdapterGroup.adptriodstcd
|
BooleanBuilder adapterWherePredicate = new BooleanBuilder(qAdapterGroup.adptriodstcd
|
||||||
.eq("I")
|
.eq("I")
|
||||||
@@ -84,6 +93,10 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
long totalCount = jpaQueryFactory
|
long totalCount = jpaQueryFactory
|
||||||
.select(qAdapterGroup.adptrbzwkgroupname.count())
|
.select(qAdapterGroup.adptrbzwkgroupname.count())
|
||||||
.from(qAdapterGroup)
|
.from(qAdapterGroup)
|
||||||
|
.leftJoin(qInflowControl)
|
||||||
|
.on(qAdapterGroup.adptrbzwkgroupname
|
||||||
|
.eq(qInflowControl.id.name)
|
||||||
|
.and(qInflowControl.id.type.eq(ADAPTER_TYPE_INFLOW)))
|
||||||
.where(boolBuilder)
|
.where(boolBuilder)
|
||||||
.fetchOne();
|
.fetchOne();
|
||||||
|
|
||||||
@@ -121,17 +134,22 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
return toDto(qAdapterGroup, qInflowControl, tuple);
|
return toDto(qAdapterGroup, qInflowControl, tuple);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<InflowControlServiceDto> findAllForInterface(Pageable pageable, String searchName) {
|
public Page<InflowControlServiceDto> findAllForInterface(Pageable pageable, String searchName, String searchUseYn) {
|
||||||
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
|
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
|
||||||
QInflowControl qInflowControl = QInflowControl.inflowControl;
|
QInflowControl qInflowControl = QInflowControl.inflowControl;
|
||||||
QEAIMessageEntity qEAIMessageEntity = QEAIMessageEntity.eAIMessageEntity;
|
QEAIMessageEntity qEAIMessageEntity = QEAIMessageEntity.eAIMessageEntity;
|
||||||
|
|
||||||
|
BooleanBuilder boolBuilder = new BooleanBuilder();
|
||||||
|
boolBuilder.and(qEAIMessageEntity.eaisvcname.contains(searchName));
|
||||||
|
if (!StringUtils.isEmpty(searchUseYn))
|
||||||
|
boolBuilder.and(useYnCondition(qInflowControl.useyn, searchUseYn));
|
||||||
|
|
||||||
List<Tuple> tupleList = jpaQueryFactory
|
List<Tuple> tupleList = jpaQueryFactory
|
||||||
.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(INTERFACE_TYPE_INFLOW).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(boolBuilder)
|
||||||
.orderBy(qEAIMessageEntity.eaisvcname.asc())
|
.orderBy(qEAIMessageEntity.eaisvcname.asc())
|
||||||
.offset(pageable.getOffset())
|
.offset(pageable.getOffset())
|
||||||
.limit(pageable.getPageSize())
|
.limit(pageable.getPageSize())
|
||||||
@@ -145,7 +163,9 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
long totalCount = jpaQueryFactory
|
long totalCount = jpaQueryFactory
|
||||||
.select(qEAIMessageEntity.eaisvcname.count())
|
.select(qEAIMessageEntity.eaisvcname.count())
|
||||||
.from(qEAIMessageEntity)
|
.from(qEAIMessageEntity)
|
||||||
.where(qEAIMessageEntity.eaisvcname.contains(searchName))
|
.leftJoin(qInflowControl)
|
||||||
|
.on(qInflowControl.id.type.eq(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
|
||||||
|
.where(boolBuilder)
|
||||||
.fetchOne();
|
.fetchOne();
|
||||||
return new PageImpl<>(dtoList, pageable, totalCount);
|
return new PageImpl<>(dtoList, pageable, totalCount);
|
||||||
}
|
}
|
||||||
@@ -199,19 +219,24 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
return toDto(qClientEntity, qInflowControl, tuple);
|
return toDto(qClientEntity, qInflowControl, tuple);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<InflowControlServiceDto> findAllForClient(Pageable pageable, String searchName) {
|
public Page<InflowControlServiceDto> findAllForClient(Pageable pageable, String searchName, String searchUseYn) {
|
||||||
|
|
||||||
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
|
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
|
||||||
QInflowControl qInflowControl = QInflowControl.inflowControl;
|
QInflowControl qInflowControl = QInflowControl.inflowControl;
|
||||||
QClientEntity qClientEntity = QClientEntity.clientEntity;
|
QClientEntity qClientEntity = QClientEntity.clientEntity;
|
||||||
|
|
||||||
|
BooleanBuilder boolBuilder = new BooleanBuilder();
|
||||||
|
boolBuilder.and(qClientEntity.clientname.contains(searchName));
|
||||||
|
if (!StringUtils.isEmpty(searchUseYn))
|
||||||
|
boolBuilder.and(useYnCondition(qInflowControl.useyn, searchUseYn));
|
||||||
|
|
||||||
List<Tuple> tupleList = jpaQueryFactory
|
List<Tuple> tupleList = jpaQueryFactory
|
||||||
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
|
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
|
||||||
.from(qClientEntity)
|
.from(qClientEntity)
|
||||||
.leftJoin(qInflowControl)
|
.leftJoin(qInflowControl)
|
||||||
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
|
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
|
||||||
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
|
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
|
||||||
.where(qClientEntity.clientname.contains(searchName))
|
.where(boolBuilder)
|
||||||
.orderBy(qClientEntity.clientname.asc())
|
.orderBy(qClientEntity.clientname.asc())
|
||||||
.offset(pageable.getOffset())
|
.offset(pageable.getOffset())
|
||||||
.limit(pageable.getPageSize())
|
.limit(pageable.getPageSize())
|
||||||
@@ -225,7 +250,10 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
long totalCount = jpaQueryFactory
|
long totalCount = jpaQueryFactory
|
||||||
.select(qClientEntity.clientname.count())
|
.select(qClientEntity.clientname.count())
|
||||||
.from(qClientEntity)
|
.from(qClientEntity)
|
||||||
.where(qClientEntity.clientname.contains(searchName))
|
.leftJoin(qInflowControl)
|
||||||
|
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
|
||||||
|
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
|
||||||
|
.where(boolBuilder)
|
||||||
.fetchOne();
|
.fetchOne();
|
||||||
return new PageImpl<>(dtoList, pageable, totalCount);
|
return new PageImpl<>(dtoList, pageable, totalCount);
|
||||||
}
|
}
|
||||||
@@ -266,11 +294,20 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
QInflowControlLog qInflowConfrolLog = QInflowControlLog.inflowControlLog;
|
QInflowControlLog qInflowConfrolLog = QInflowControlLog.inflowControlLog;
|
||||||
QInflowControlGroup qinflowControlGroup = QInflowControlGroup.inflowControlGroup;
|
QInflowControlGroup qinflowControlGroup = QInflowControlGroup.inflowControlGroup;
|
||||||
|
|
||||||
|
String startTime = StringUtils.defaultIfEmpty(uiSearch.getSearchStartTime(), "000000");
|
||||||
|
String endTime = StringUtils.defaultIfEmpty(uiSearch.getSearchEndTime(), "235959");
|
||||||
|
|
||||||
BooleanBuilder predicate = new BooleanBuilder();
|
BooleanBuilder predicate = new BooleanBuilder();
|
||||||
predicate
|
predicate
|
||||||
.and(qInflowConfrolLog.id.msgdpstyms
|
.and(qInflowConfrolLog.id.msgdpstyms
|
||||||
.between(uiSearch.getSearchStartDate() + "000000000",
|
.between(uiSearch.getSearchStartDate() + startTime + "000",
|
||||||
uiSearch.getSearchEndDate() + "235959999"));
|
uiSearch.getSearchEndDate() + endTime + "999"));
|
||||||
|
if (!StringUtils.isEmpty(uiSearch.getSearchInstanceName()))
|
||||||
|
predicate.and(qInflowConfrolLog.eaisevrinstncname.contains(uiSearch.getSearchInstanceName()));
|
||||||
|
if (!StringUtils.isEmpty(uiSearch.getSearchApiName()))
|
||||||
|
predicate.and(qInflowConfrolLog.eaisvcname.contains(uiSearch.getSearchApiName()));
|
||||||
|
if (!StringUtils.isEmpty(uiSearch.getSearchAdapterGroupName()))
|
||||||
|
predicate.and(qInflowConfrolLog.adptrbzwkgroupname.contains(uiSearch.getSearchAdapterGroupName()));
|
||||||
|
|
||||||
return getJPAQueryFactory()
|
return getJPAQueryFactory()
|
||||||
.select(
|
.select(
|
||||||
|
|||||||
+656
-8
@@ -1,18 +1,122 @@
|
|||||||
package com.eactive.eai.rms.ext.djb.apistatus;
|
package com.eactive.eai.rms.ext.djb.apistatus;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.common.entity.Auditable;
|
||||||
|
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
|
||||||
|
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentApi;
|
||||||
|
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncidentTimeline;
|
||||||
|
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentKind;
|
||||||
|
import com.eactive.apim.portal.djb.apistatus.incident.entity.IncidentState;
|
||||||
|
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
|
||||||
|
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository;
|
||||||
|
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentTimelineRepository;
|
||||||
|
import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
|
||||||
|
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeService;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 상태 탐지 결과를 개발자포탈 API Status 화면용 장애 데이터로 반영한다.
|
||||||
|
*
|
||||||
|
* <p>탐지 자체는 {@link ApiStatusService} 가 수행하며, 본 서비스는 그 결과를
|
||||||
|
* {@code DJB_APISTATUS_INCIDENT} / {@code _API} / {@code _TIMELINE} + {@code PTL_NOTICE} 로 기록한다.</p>
|
||||||
|
*
|
||||||
|
* <p>자동 등록 여부와 공지 게시 시점은 PTL_PROPERTY
|
||||||
|
* ({@code Portal} / {@code djb.apistatus.auto-incident-mode}) 로 정한다.
|
||||||
|
* {@code OFF}면 자동 등록을 하지 않고, {@code DRAFT}(기본)면 관리자가 공지사항 관리에서
|
||||||
|
* 검수·게시해야 개발자포탈에 노출되며, {@code PUBLISH}면 탐지 즉시 노출된다.
|
||||||
|
* {@code OFF} 여도 이미 열려있는 장애의 복구 처리(ERROR_END/DELAY_END)는 계속 동작한다.</p>
|
||||||
|
*
|
||||||
|
* <p>점검(CONTROL_*) 은 관리자가 공지사항으로 등록한 점검 일정이 원천이다
|
||||||
|
* ({@code ApiStatusRepository.findApiStatusEvents} 가 INCIDENT 기간을 읽어 CTRL_YN 을 판정).
|
||||||
|
* 따라서 CONTROL 이벤트를 다시 기록하면 순환이 되므로 기록하지 않는다.</p>
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional(transactionManager = "transactionManagerForEMS", rollbackFor = Exception.class)
|
||||||
public class ApiStatusDetectionService {
|
public class ApiStatusDetectionService {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class);
|
private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class);
|
||||||
|
|
||||||
|
public static final String EVENT_CONTROL_START = "CONTROL_START";
|
||||||
|
public static final String EVENT_CONTROL_END = "CONTROL_END";
|
||||||
|
public static final String EVENT_ERROR_START = "ERROR_START";
|
||||||
|
public static final String EVENT_ERROR_END = "ERROR_END";
|
||||||
|
public static final String EVENT_DELAY_START = "DELAY_START";
|
||||||
|
public static final String EVENT_DELAY_END = "DELAY_END";
|
||||||
|
|
||||||
|
/** INTERFACE_ID 멱등 윈도우 (분) */
|
||||||
|
private static final int IDEMPOTENT_WINDOW_MINUTES = 5;
|
||||||
|
|
||||||
|
/** 종결 상태 - 신규 탐지 병합 대상에서 제외 */
|
||||||
|
private static final List<IncidentState> CLOSED_STATES =
|
||||||
|
Collections.unmodifiableList(Arrays.asList(IncidentState.RESOLVED, IncidentState.CANCELED));
|
||||||
|
|
||||||
|
private static final String AUTHOR_SYSTEM = "SYSTEM";
|
||||||
|
private static final String DETECTED_BY_AUTO = "AUTO";
|
||||||
|
private static final String NOTICE_TYPE_INCIDENT = "3";
|
||||||
|
private static final int NOTICE_SUBJECT_MAX_LENGTH = 255;
|
||||||
|
|
||||||
|
/** 자동 탐지 공지 처리 방식 프로퍼티 그룹 (개발자포탈과 동일 그룹을 쓴다) */
|
||||||
|
public static final String PROPERTY_GROUP = "Portal";
|
||||||
|
|
||||||
|
/** 자동 탐지 장애/지연의 공지 처리 방식 - OFF / DRAFT / PUBLISH */
|
||||||
|
public static final String KEY_AUTO_INCIDENT_MODE = "djb.apistatus.auto-incident-mode";
|
||||||
|
|
||||||
|
/** 자동 등록 안 함. 신규 등록·병합 모두 하지 않는다 (복구 처리는 계속 동작) */
|
||||||
|
public static final String MODE_OFF = "OFF";
|
||||||
|
|
||||||
|
/** 초안만 생성. 관리자가 공지사항 관리에서 검수·게시해야 개발자포탈에 노출된다 */
|
||||||
|
public static final String MODE_DRAFT = "DRAFT";
|
||||||
|
|
||||||
|
/** 초안 생성 후 곧바로 게시. 검수 없이 개발자포탈에 즉시 노출된다 */
|
||||||
|
public static final String MODE_PUBLISH = "PUBLISH";
|
||||||
|
|
||||||
|
/** 오탐이 그대로 고객에게 노출되지 않도록 기본은 검수(DRAFT) */
|
||||||
|
private static final String DEFAULT_AUTO_INCIDENT_MODE = MODE_DRAFT;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DjbApistatusIncidentRepository incidentRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DjbApistatusIncidentApiRepository incidentApiRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DjbApistatusIncidentTimelineRepository incidentTimelineRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PortalNoticeService portalNoticeService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApiStatusRepository apiStatusRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PortalPropertyService portalPropertyService;
|
||||||
|
|
||||||
|
/** 자동 초안 문구 양식 (classpath:apistatus-draft.yml) */
|
||||||
|
@Autowired
|
||||||
|
private ApiStatusDraftTemplate draftTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이벤트 감지
|
* 이벤트 감지
|
||||||
* @param event "CONTROL_START" : 점검시작
|
* @param event "CONTROL_START" : 점검시작
|
||||||
@@ -25,17 +129,561 @@ public class ApiStatusDetectionService {
|
|||||||
*/
|
*/
|
||||||
public void detect(String event, List<String> apiIds) {
|
public void detect(String event, List<String> apiIds) {
|
||||||
log.debug("이벤트 감지: {} {}", event, apiIds);
|
log.debug("이벤트 감지: {} {}", event, apiIds);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(event) || apiIds == null || apiIds.isEmpty()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API FSM 내 아직 장애로 남아있는 API 목록
|
List<String> targets = apiIds.stream()
|
||||||
public void remainDownApiIds() {
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (event) {
|
||||||
|
case EVENT_ERROR_START:
|
||||||
|
openIncident(event, IncidentKind.INCIDENT, targets, "에러율 임계 초과", "장애");
|
||||||
|
break;
|
||||||
|
case EVENT_DELAY_START:
|
||||||
|
openIncident(event, IncidentKind.DELAY, targets, "응답시간 임계 초과", "응답지연");
|
||||||
|
break;
|
||||||
|
case EVENT_ERROR_END:
|
||||||
|
case EVENT_DELAY_END:
|
||||||
|
markRecovered(event, targets);
|
||||||
|
break;
|
||||||
|
case EVENT_CONTROL_START:
|
||||||
|
case EVENT_CONTROL_END:
|
||||||
|
// 점검은 관리자 등록 공지가 원천 - 재기록하지 않음
|
||||||
|
log.debug("점검 이벤트는 장애 데이터로 기록하지 않음: {} {}", event, targets);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn("알 수 없는 이벤트: {}", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 정상 동작 - detect()에서 일괄 처리 (복구 = 점검종료, 장애종료, 지연종료)
|
/**
|
||||||
// public void recovered(String event, String[] apiIds) {
|
* 미종결 장애·지연으로 남아있는 API 목록 (탐지 Job 폴링용)
|
||||||
//
|
*/
|
||||||
// }
|
@Transactional(transactionManager = "transactionManagerForEMS", readOnly = true)
|
||||||
|
public List<String> remainDownApiIds() {
|
||||||
|
List<DjbApistatusIncident> openIncidents = incidentRepository
|
||||||
|
.findByKindInAndStateNotInOrderByStartedAtDesc(IncidentKind.DEGRADING, CLOSED_STATES);
|
||||||
|
if (openIncidents.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> incidentIds = openIncidents.stream()
|
||||||
|
.map(DjbApistatusIncident::getIncidentId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return incidentApiRepository.findByIncidentIdInOrderByIncidentIdAscApiIdAsc(incidentIds).stream()
|
||||||
|
.filter(api -> api.getRecoveredAt() == null)
|
||||||
|
.map(DjbApistatusIncidentApi::getApiId)
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── 장애 발생 ──────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 신규 이슈 등록. 지연(DELAY)은 공지를 만들지 않고, 장애(INCIDENT)만 공지 초안/게시를 동반한다.
|
||||||
|
*/
|
||||||
|
private void openIncident(String event, IncidentKind kind, List<String> apiIds,
|
||||||
|
String summary, String titleKeyword) {
|
||||||
|
// 0) OFF 면 신규 등록·병합·타임라인 기록을 모두 건너뛴다.
|
||||||
|
// 이미 열려있는 이슈의 복구(markRecovered)는 계속 동작해야 하므로 여기서만 막는다.
|
||||||
|
String mode = resolveAutoIncidentMode();
|
||||||
|
if (MODE_OFF.equals(mode)) {
|
||||||
|
log.info("자동 이슈 등록 비활성(OFF) - 기록하지 않음: {} {}", event, apiIds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean autoPublish = MODE_PUBLISH.equals(mode);
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
String interfaceId = makeInterfaceId(event, apiIds, now);
|
||||||
|
|
||||||
|
// 1) 동일 윈도우 재탐지 - 타임라인만 남기고 종료
|
||||||
|
Optional<DjbApistatusIncident> duplicated = incidentRepository.findByInterfaceId(interfaceId);
|
||||||
|
if (duplicated.isPresent()) {
|
||||||
|
appendTimeline(duplicated.get().getIncidentId(), null,
|
||||||
|
draftTemplate.text(ApiStatusDraftTemplate.TL_REDETECTED, "event", event), now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 이미 열려있는 자동 등록 이슈가 해당 API 를 포함하면 영향 API 만 병합
|
||||||
|
// (관리자가 직접 작성한 장애 공지는 건드리지 않는다)
|
||||||
|
// 지연으로 열린 건에 장애가 얹히는 경우가 있어 지연·장애를 함께 찾는다.
|
||||||
|
List<DjbApistatusIncident> openIncidents = incidentRepository
|
||||||
|
.findOpenByApiIds(IncidentKind.DEGRADING, DETECTED_BY_AUTO, CLOSED_STATES, apiIds);
|
||||||
|
if (!openIncidents.isEmpty()) {
|
||||||
|
mergeIntoOpenIncident(openIncidents.get(0), event, kind, apiIds, now, autoPublish);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 신규 이슈 생성. 장애는 공지도 같이 만들고, PUBLISH 면 검수 없이 바로 게시한다.
|
||||||
|
Map<String, String> apiNames = resolveApiNames(apiIds);
|
||||||
|
String title = buildTitle(titleKeyword, apiIds, apiNames);
|
||||||
|
|
||||||
|
PortalNotice notice = kind == IncidentKind.DELAY
|
||||||
|
? null : createNotice(title, summary, apiIds, apiNames, now, autoPublish);
|
||||||
|
|
||||||
|
DjbApistatusIncident incident = new DjbApistatusIncident();
|
||||||
|
incident.setKind(kind);
|
||||||
|
incident.setState(IncidentState.INVESTIGATING);
|
||||||
|
incident.setTitle(title);
|
||||||
|
incident.setSummary(summary);
|
||||||
|
incident.setStartedAt(now);
|
||||||
|
incident.setDetectedBy(DETECTED_BY_AUTO);
|
||||||
|
incident.setInterfaceId(interfaceId);
|
||||||
|
incident.setNoticeId(notice == null ? null : notice.getId());
|
||||||
|
// 개발자포탈 노출 조건: 공지가 있으면 DRAFT_YN='N' + USE_YN='Y', 없으면(지연) DRAFT_YN='N' 만
|
||||||
|
incident.setDraftYn(autoPublish ? "N" : "Y");
|
||||||
|
incident.setFixYn("N");
|
||||||
|
stampAudit(incident, now);
|
||||||
|
|
||||||
|
DjbApistatusIncident saved;
|
||||||
|
try {
|
||||||
|
saved = incidentRepository.saveAndFlush(incident);
|
||||||
|
} catch (DataIntegrityViolationException e) {
|
||||||
|
// INTERFACE_ID UNIQUE 충돌 - 동시 탐지. 만든 공지를 정리하고 타임라인만 남긴다
|
||||||
|
log.info("이슈 중복 감지 (interfaceId={}) - 기존 이슈에 타임라인 추가", interfaceId, e);
|
||||||
|
if (notice != null) {
|
||||||
|
portalNoticeService.deleteById(notice.getId());
|
||||||
|
}
|
||||||
|
incidentRepository.findByInterfaceId(interfaceId).ifPresent(existing ->
|
||||||
|
appendTimeline(existing.getIncidentId(), null,
|
||||||
|
draftTemplate.text(ApiStatusDraftTemplate.TL_REDETECTED, "event", event), now));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveIncidentApis(saved.getIncidentId(), apiIds, apiNames, now);
|
||||||
|
// 타임라인도 고객 화면에 그대로 나가므로 인터페이스 ID 대신 게시 API 명 + 건수로 적는다
|
||||||
|
appendTimeline(saved.getIncidentId(), IncidentState.INVESTIGATING,
|
||||||
|
draftTemplate.text(ApiStatusDraftTemplate.TL_DETECTED,
|
||||||
|
"summary", summary, "event", event, "affected", affectedSummary(apiIds, apiNames)), now);
|
||||||
|
|
||||||
|
log.info("자동 이슈 등록: incidentId={}, kind={}, noticeId={}, mode={}, apis={}",
|
||||||
|
saved.getIncidentId(), kind, notice == null ? "-" : notice.getId(),
|
||||||
|
autoPublish ? MODE_PUBLISH : MODE_DRAFT, apiIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 자동 탐지 장애/지연의 공지 처리 방식.
|
||||||
|
*
|
||||||
|
* <p>PTL_PROPERTY({@code Portal} / {@code djb.apistatus.auto-incident-mode}) 를 읽는다.
|
||||||
|
* 최초 접근 시 기본값으로 row 가 자동 생성된다.</p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code OFF} - 자동 등록 안 함 (복구 처리는 계속)</li>
|
||||||
|
* <li>{@code DRAFT}(기본) - 초안만 생성, 관리자 검수 후 게시</li>
|
||||||
|
* <li>{@code PUBLISH} - 초안 생성 후 즉시 게시</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>조회 실패나 알 수 없는 값은 검수 쪽(DRAFT)으로 떨어뜨린다 - 오탐이 고객 화면에
|
||||||
|
* 그대로 나가는 것도, 진짜 장애를 아예 안 남기는 것도 기본 동작으로는 위험하기 때문이다.</p>
|
||||||
|
*/
|
||||||
|
private String resolveAutoIncidentMode() {
|
||||||
|
try {
|
||||||
|
String value = portalPropertyService.getOrCreateProperty(
|
||||||
|
PROPERTY_GROUP, KEY_AUTO_INCIDENT_MODE, DEFAULT_AUTO_INCIDENT_MODE,
|
||||||
|
"API 상태 자동 탐지 공지 처리 방식 - OFF: 자동 등록 안 함, "
|
||||||
|
+ "DRAFT: 초안만 생성(관리자 검수 후 게시), PUBLISH: 초안 생성 후 즉시 게시");
|
||||||
|
String normalized = StringUtils.trimToEmpty(value);
|
||||||
|
if (MODE_OFF.equalsIgnoreCase(normalized)) {
|
||||||
|
return MODE_OFF;
|
||||||
|
}
|
||||||
|
if (MODE_PUBLISH.equalsIgnoreCase(normalized)) {
|
||||||
|
return MODE_PUBLISH;
|
||||||
|
}
|
||||||
|
if (!MODE_DRAFT.equalsIgnoreCase(normalized)) {
|
||||||
|
log.warn("알 수 없는 자동 탐지 공지 처리 방식 '{}' - 기본값 {} 적용", value, DEFAULT_AUTO_INCIDENT_MODE);
|
||||||
|
}
|
||||||
|
return MODE_DRAFT;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("자동 탐지 공지 처리 방식 조회 실패 - 기본값 {} 적용", DEFAULT_AUTO_INCIDENT_MODE, e);
|
||||||
|
return DEFAULT_AUTO_INCIDENT_MODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeIntoOpenIncident(DjbApistatusIncident incident, String event, IncidentKind kind,
|
||||||
|
List<String> apiIds, LocalDateTime now, boolean autoPublish) {
|
||||||
|
Long incidentId = incident.getIncidentId();
|
||||||
|
|
||||||
|
// 지연으로 열린 건에 장애가 얹히면 종류를 올린다 (강등은 하지 않는다)
|
||||||
|
boolean escalated = escalateIfNeeded(incident, event, kind, now, autoPublish);
|
||||||
|
|
||||||
|
// DRAFT 로 만들어진 뒤 PUBLISH 로 설정이 바뀐 경우, 이 이슈가 닫힐 때까지 모든 탐지가
|
||||||
|
// 여기로 병합돼 아무것도 게시되지 않는다. 병합 시점에 게시 상태를 맞춰준다.
|
||||||
|
if (autoPublish) {
|
||||||
|
publishIfPending(incident);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> mapped = incidentApiRepository.findByIncidentIdOrderByApiId(incidentId).stream()
|
||||||
|
.map(DjbApistatusIncidentApi::getApiId)
|
||||||
|
.collect(Collectors.toCollection(HashSet::new));
|
||||||
|
|
||||||
|
List<String> added = apiIds.stream()
|
||||||
|
.filter(apiId -> !mapped.contains(apiId))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (added.isEmpty()) {
|
||||||
|
// 격상을 남긴 턴에는 같은 시각·같은 이벤트로 "재탐지 신호 수신"이 겹쳐 찍힌다.
|
||||||
|
// 격상 줄이 더 구체적인 정보이므로 재탐지 줄은 생략한다.
|
||||||
|
if (!escalated) {
|
||||||
|
appendTimeline(incidentId, null,
|
||||||
|
draftTemplate.text(ApiStatusDraftTemplate.TL_REDETECTED, "event", event), now);
|
||||||
|
}
|
||||||
|
log.debug("기존 이슈에 이미 포함된 API - 타임라인만 추가: incidentId={}, apis={}, escalated={}",
|
||||||
|
incidentId, apiIds, escalated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> addedNames = resolveApiNames(added);
|
||||||
|
saveIncidentApis(incidentId, added, addedNames, now);
|
||||||
|
appendTimeline(incidentId, null,
|
||||||
|
draftTemplate.text(ApiStatusDraftTemplate.TL_API_ADDED,
|
||||||
|
"event", event, "affected", affectedSummary(added, addedNames)), now);
|
||||||
|
log.info("기존 이슈에 영향 API 병합: incidentId={}, addedApis={}", incidentId, added);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지연(DELAY)으로 열린 이슈에 장애(ERROR_START)가 감지되면 종류를 장애로 올린다.
|
||||||
|
*
|
||||||
|
* <p>지연은 공지가 없으므로 격상 시점에 공지를 새로 만들어 붙인다. 반대 방향(장애→지연)은
|
||||||
|
* 하지 않는다 - 한 번 장애로 인지된 구간을 나중에 완화해서 표기하면 이력이 왜곡된다.</p>
|
||||||
|
*
|
||||||
|
* @return 실제로 격상하여 타임라인을 남겼으면 true
|
||||||
|
*/
|
||||||
|
private boolean escalateIfNeeded(DjbApistatusIncident incident, String event, IncidentKind kind,
|
||||||
|
LocalDateTime now, boolean autoPublish) {
|
||||||
|
if (kind != IncidentKind.INCIDENT || incident.getKind() != IncidentKind.DELAY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> apiIds = incidentApiRepository.findByIncidentIdOrderByApiId(incident.getIncidentId()).stream()
|
||||||
|
.map(DjbApistatusIncidentApi::getApiId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<String, String> apiNames = resolveApiNames(apiIds);
|
||||||
|
String title = buildTitle("장애", apiIds, apiNames);
|
||||||
|
String summary = "에러율 임계 초과";
|
||||||
|
|
||||||
|
incident.setKind(IncidentKind.INCIDENT);
|
||||||
|
incident.setTitle(title);
|
||||||
|
incident.setSummary(summary);
|
||||||
|
if (incident.getNoticeId() == null) {
|
||||||
|
// 이미 노출 중이던(DRAFT_YN='N') 지연이 격상되는데 공지가 미게시로 붙으면
|
||||||
|
// 보이던 이슈가 사라진다. 그 경우엔 공지도 게시 상태로 만든다.
|
||||||
|
boolean publishNotice = autoPublish || "N".equals(incident.getDraftYn());
|
||||||
|
PortalNotice notice = createNotice(title, summary, apiIds, apiNames, now, publishNotice);
|
||||||
|
incident.setNoticeId(notice.getId());
|
||||||
|
}
|
||||||
|
incident.setLastModifiedBy(AUTHOR_SYSTEM);
|
||||||
|
incident.setLastModifiedDate(now);
|
||||||
|
incidentRepository.save(incident);
|
||||||
|
|
||||||
|
appendTimeline(incident.getIncidentId(), null,
|
||||||
|
draftTemplate.text(ApiStatusDraftTemplate.TL_ESCALATED, "event", event), now);
|
||||||
|
log.info("지연 → 장애 격상: incidentId={}, noticeId={}",
|
||||||
|
incident.getIncidentId(), incident.getNoticeId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 아직 초안으로 남아있는 자동 탐지 이슈를 게시 상태로 올린다 (PUBLISH 모드에서만 호출).
|
||||||
|
*
|
||||||
|
* <p>대상은 자동 등록(DETECTED_BY='AUTO') 건뿐이다. 관리자가 공지사항 관리에서 손댄 건은
|
||||||
|
* DETECTED_BY 가 'MANUAL' 로 바뀌므로 여기 오지 않는다.</p>
|
||||||
|
*
|
||||||
|
* <p>지연은 연결 공지가 없으므로 DRAFT_YN 만 바꾸면 노출된다.</p>
|
||||||
|
*/
|
||||||
|
private void publishIfPending(DjbApistatusIncident incident) {
|
||||||
|
if (!"Y".equals(incident.getDraftYn())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
incident.setDraftYn("N");
|
||||||
|
incidentRepository.save(incident);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(incident.getNoticeId())) {
|
||||||
|
portalNoticeService.findById(incident.getNoticeId())
|
||||||
|
.filter(notice -> !"Y".equals(notice.getUseYn()))
|
||||||
|
.ifPresent(notice -> {
|
||||||
|
notice.setUseYn("Y");
|
||||||
|
portalNoticeService.save(notice);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.info("자동 탐지 이슈 뒤늦은 게시: incidentId={}, noticeId={}",
|
||||||
|
incident.getIncidentId(), incident.getNoticeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── 복구 ──────────────
|
||||||
|
|
||||||
|
private void markRecovered(String event, List<String> apiIds) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
List<DjbApistatusIncidentApi> candidates =
|
||||||
|
incidentApiRepository.findByApiIdInAndRecoveredAtIsNull(apiIds);
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
log.debug("복구 대상 장애 없음: {} {}", event, apiIds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 복구는 이벤트 종류를 구분하지 않는다. ERROR_END 하나로 그 API 의 지연 건까지 닫는다
|
||||||
|
// (상태머신상 E → N 으로 돌아가므로 DELAY_END 가 따로 오지 않는다).
|
||||||
|
Map<Long, DjbApistatusIncident> openIncidents = incidentRepository
|
||||||
|
.findByKindInAndStateNotInOrderByStartedAtDesc(IncidentKind.DEGRADING, CLOSED_STATES).stream()
|
||||||
|
.collect(Collectors.toMap(DjbApistatusIncident::getIncidentId, incident -> incident));
|
||||||
|
|
||||||
|
Map<Long, List<String>> recoveredByIncident = new HashMap<>();
|
||||||
|
for (DjbApistatusIncidentApi api : candidates) {
|
||||||
|
if (!openIncidents.containsKey(api.getIncidentId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
api.setRecoveredAt(now);
|
||||||
|
api.setLastModifiedBy(AUTHOR_SYSTEM);
|
||||||
|
api.setLastModifiedDate(now);
|
||||||
|
incidentApiRepository.save(api);
|
||||||
|
|
||||||
|
recoveredByIncident.computeIfAbsent(api.getIncidentId(), key -> new ArrayList<>())
|
||||||
|
.add(api.getApiId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recoveredByIncident.isEmpty()) {
|
||||||
|
log.debug("복구 대상 장애 없음 (미종결 장애 미포함): {} {}", event, apiIds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<Long, List<String>> entry : recoveredByIncident.entrySet()) {
|
||||||
|
Long incidentId = entry.getKey();
|
||||||
|
List<String> recoveredApis = entry.getValue();
|
||||||
|
DjbApistatusIncident incident = openIncidents.get(incidentId);
|
||||||
|
|
||||||
|
boolean allRecovered = incidentApiRepository.countByIncidentIdAndRecoveredAtIsNull(incidentId) == 0;
|
||||||
|
IncidentState nextState = allRecovered ? IncidentState.RESOLVED : IncidentState.MONITORING;
|
||||||
|
|
||||||
|
if (incident.getState() != nextState) {
|
||||||
|
incident.setPreviousState(incident.getState());
|
||||||
|
incident.setState(nextState);
|
||||||
|
}
|
||||||
|
if (allRecovered) {
|
||||||
|
incident.setEndAt(now);
|
||||||
|
}
|
||||||
|
incident.setLastModifiedBy(AUTHOR_SYSTEM);
|
||||||
|
incident.setLastModifiedDate(now);
|
||||||
|
incidentRepository.save(incident);
|
||||||
|
|
||||||
|
String body = draftTemplate.text(
|
||||||
|
allRecovered ? ApiStatusDraftTemplate.TL_RECOVERED_ALL
|
||||||
|
: ApiStatusDraftTemplate.TL_RECOVERED_PART,
|
||||||
|
"event", event,
|
||||||
|
"affected", affectedSummary(recoveredApis, resolveApiNames(recoveredApis)));
|
||||||
|
appendTimeline(incidentId, nextState, body, now);
|
||||||
|
|
||||||
|
log.info("자동 복구 처리: incidentId={}, state={}, apis={}", incidentId, nextState, recoveredApis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── 내부 헬퍼 ──────────────
|
||||||
|
|
||||||
|
private String makeInterfaceId(String event, List<String> sortedApiIds, LocalDateTime now) {
|
||||||
|
long windowSeconds = IDEMPOTENT_WINDOW_MINUTES * 60L;
|
||||||
|
long flooredEpoch = now.atZone(ZoneId.systemDefault()).toEpochSecond()
|
||||||
|
/ windowSeconds * windowSeconds;
|
||||||
|
String joined = String.join(",", sortedApiIds);
|
||||||
|
String interfaceId = event + ":" + joined + ":" + flooredEpoch;
|
||||||
|
|
||||||
|
// INTERFACE_ID 는 VARCHAR2(200) - 초과 시 API 목록을 해시로 축약
|
||||||
|
if (interfaceId.length() > 200) {
|
||||||
|
interfaceId = event + ":#" + Integer.toHexString(joined.hashCode())
|
||||||
|
+ ":" + sortedApiIds.size() + ":" + flooredEpoch;
|
||||||
|
}
|
||||||
|
return interfaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> resolveApiNames(List<String> apiIds) {
|
||||||
|
Map<String, String> names = new HashMap<>();
|
||||||
|
try {
|
||||||
|
for (ApiNameRow row : apiStatusRepository.findApiNames(apiIds)) {
|
||||||
|
names.put(row.getEaisvcname(), row.getEaisvcdesc());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("API 명 조회 실패 - API ID 를 그대로 사용", e);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String apiLabel(String apiId, Map<String, String> apiNames) {
|
||||||
|
return StringUtils.defaultIfBlank(apiNames.get(apiId), apiId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 영향 인터페이스를 개발자포탈 게시 여부로 가른 결과.
|
||||||
|
* 게시된 것(= API)만 이름을 드러내고 나머지는 건수로만 표기한다.
|
||||||
|
*/
|
||||||
|
private static final class ApiSplit {
|
||||||
|
private final List<String> published;
|
||||||
|
private final int hiddenCount;
|
||||||
|
|
||||||
|
private ApiSplit(List<String> published, int hiddenCount) {
|
||||||
|
this.published = published;
|
||||||
|
this.hiddenCount = hiddenCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 게시 인터페이스 판정. 조회에 실패하면 전부 비게시로 보아 ID·명칭 노출을 막는다. */
|
||||||
|
private ApiSplit splitPublished(List<String> apiIds) {
|
||||||
|
if (apiIds == null || apiIds.isEmpty()) {
|
||||||
|
return new ApiSplit(Collections.emptyList(), 0);
|
||||||
|
}
|
||||||
|
Set<String> publishedIds;
|
||||||
|
try {
|
||||||
|
publishedIds = new HashSet<>(apiStatusRepository.findPublishedApiIds(apiIds));
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 실패 시 노출하는 쪽으로 기울면 내부 인터페이스가 고객 화면에 새어 나간다
|
||||||
|
log.warn("게시 API 판정 실패 - 전부 GW 인터페이스로 묶어 표기", e);
|
||||||
|
publishedIds = Collections.emptySet();
|
||||||
|
}
|
||||||
|
List<String> published = new ArrayList<>();
|
||||||
|
for (String apiId : apiIds) {
|
||||||
|
if (publishedIds.contains(apiId)) {
|
||||||
|
published.add(apiId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ApiSplit(published, apiIds.size() - published.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "GW 인터페이스 3건" - 게시되지 않은 인터페이스 묶음 표기 */
|
||||||
|
private String hiddenLabel(int hiddenCount) {
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.GW_LABEL, "count", String.valueOf(hiddenCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 영향 인터페이스 한 줄 요약. 게시 API 는 이름으로, 나머지는 건수로 묶는다.
|
||||||
|
* 예) {@code 계좌조회, 이체 · GW 인터페이스 2건}
|
||||||
|
*/
|
||||||
|
private String affectedSummary(List<String> apiIds, Map<String, String> apiNames) {
|
||||||
|
ApiSplit split = splitPublished(apiIds);
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
for (String apiId : split.published) {
|
||||||
|
parts.add(apiLabel(apiId, apiNames));
|
||||||
|
}
|
||||||
|
String names = String.join(", ", parts);
|
||||||
|
if (split.hiddenCount == 0) {
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
return names.isEmpty() ? hiddenLabel(split.hiddenCount) : names + " · " + hiddenLabel(split.hiddenCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공지 제목. 게시 API 가 하나도 없으면 인터페이스 명을 쓰지 않고 건수로만 적는다.
|
||||||
|
*/
|
||||||
|
private String buildTitle(String titleKeyword, List<String> apiIds, Map<String, String> apiNames) {
|
||||||
|
ApiSplit split = splitPublished(apiIds);
|
||||||
|
if (split.published.isEmpty()) {
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_HIDDEN_ONLY,
|
||||||
|
"gw", hiddenLabel(apiIds.size()), "keyword", titleKeyword);
|
||||||
|
}
|
||||||
|
String first = apiLabel(split.published.get(0), apiNames);
|
||||||
|
if (apiIds.size() > 1) {
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_MULTI,
|
||||||
|
"first", first, "rest", String.valueOf(apiIds.size() - 1), "keyword", titleKeyword);
|
||||||
|
}
|
||||||
|
return draftTemplate.text(ApiStatusDraftTemplate.TITLE_SINGLE, "first", first, "keyword", titleKeyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 자동 탐지 장애 공지 생성.
|
||||||
|
*
|
||||||
|
* <p>본문에는 관리자가 검수하며 채울 항목(발생 원인·영향 범위·조치 내용·예상 복구 시간)을
|
||||||
|
* 미리 넣어 둔다. 초안이면 작성 힌트를 함께 남기고, 이미 게시되는 경우엔 고객에게
|
||||||
|
* 그대로 노출되므로 "확인 중입니다" 로 채워 둔다.</p>
|
||||||
|
*
|
||||||
|
* @param publish true 면 게시(USE_YN='Y'), false 면 초안(USE_YN='N')
|
||||||
|
*/
|
||||||
|
private PortalNotice createNotice(String title, String summary, List<String> apiIds,
|
||||||
|
Map<String, String> apiNames, LocalDateTime now, boolean publish) {
|
||||||
|
StringBuilder detail = new StringBuilder();
|
||||||
|
detail.append(draftTemplate.text(publish
|
||||||
|
? ApiStatusDraftTemplate.BODY_LEAD_PUBLISH
|
||||||
|
: ApiStatusDraftTemplate.BODY_LEAD_DRAFT,
|
||||||
|
"summary", summary));
|
||||||
|
|
||||||
|
// 게시된 API 만 이름으로 적고(인터페이스 ID 는 넣지 않는다) 나머지는 건수로 묶는다.
|
||||||
|
// 공지 본문은 저장 시점에 굳는 HTML 이라 나중에 사용자별로 가릴 수 없다.
|
||||||
|
ApiSplit split = splitPublished(apiIds);
|
||||||
|
detail.append("<p><b>")
|
||||||
|
.append(draftTemplate.text(ApiStatusDraftTemplate.BODY_AFFECTED_HEADING))
|
||||||
|
.append("</b></p><ul>");
|
||||||
|
for (String apiId : split.published) {
|
||||||
|
detail.append("<li>").append(apiLabel(apiId, apiNames)).append("</li>");
|
||||||
|
}
|
||||||
|
if (split.hiddenCount > 0) {
|
||||||
|
detail.append("<li>").append(hiddenLabel(split.hiddenCount)).append("</li>");
|
||||||
|
}
|
||||||
|
detail.append("</ul>");
|
||||||
|
|
||||||
|
for (ApiStatusDraftTemplate.Section section : draftTemplate.getSections()) {
|
||||||
|
detail.append("<p><b>").append(section.getTitle()).append("</b></p>");
|
||||||
|
// 게시 상태로 나가는 본문에 "작성 필요" 가 그대로 보이면 안 되므로 문구를 나눈다
|
||||||
|
detail.append("<p>").append(publish
|
||||||
|
? draftTemplate.text(ApiStatusDraftTemplate.BODY_FILLED_TEXT)
|
||||||
|
: draftTemplate.text(ApiStatusDraftTemplate.BODY_DRAFT_TEXT, "hint", section.getHint()))
|
||||||
|
.append("</p>");
|
||||||
|
}
|
||||||
|
|
||||||
|
PortalNotice notice = new PortalNotice();
|
||||||
|
notice.setId(null);
|
||||||
|
notice.setNoticeSubject(StringUtils.abbreviate(title, NOTICE_SUBJECT_MAX_LENGTH));
|
||||||
|
notice.setNoticeDetail(detail.toString());
|
||||||
|
notice.setNoticeType(NOTICE_TYPE_INCIDENT);
|
||||||
|
notice.setUseYn(publish ? "Y" : "N");
|
||||||
|
notice.setFixYn("N");
|
||||||
|
notice.setReadCount(0L);
|
||||||
|
notice.setInquirerName(AUTHOR_SYSTEM);
|
||||||
|
notice.setCreatedBy(AUTHOR_SYSTEM);
|
||||||
|
notice.setCreatedDate(now);
|
||||||
|
|
||||||
|
portalNoticeService.save(notice);
|
||||||
|
return notice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveIncidentApis(Long incidentId, Collection<String> apiIds,
|
||||||
|
Map<String, String> apiNames, LocalDateTime now) {
|
||||||
|
List<DjbApistatusIncidentApi> rows = new ArrayList<>();
|
||||||
|
for (String apiId : apiIds) {
|
||||||
|
DjbApistatusIncidentApi api = new DjbApistatusIncidentApi();
|
||||||
|
api.setIncidentId(incidentId);
|
||||||
|
api.setApiId(apiId);
|
||||||
|
api.setApiName(apiNames.get(apiId));
|
||||||
|
stampAudit(api, now);
|
||||||
|
rows.add(api);
|
||||||
|
}
|
||||||
|
if (!rows.isEmpty()) {
|
||||||
|
incidentApiRepository.saveAll(rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendTimeline(Long incidentId, IncidentState stateAfter, String body, LocalDateTime now) {
|
||||||
|
DjbApistatusIncidentTimeline timeline = new DjbApistatusIncidentTimeline();
|
||||||
|
timeline.setIncidentId(incidentId);
|
||||||
|
timeline.setEventAt(now);
|
||||||
|
timeline.setStateAfter(stateAfter);
|
||||||
|
timeline.setBody(StringUtils.abbreviate(body, 4000));
|
||||||
|
timeline.setAuthorType(AUTHOR_SYSTEM);
|
||||||
|
timeline.setVisibleYn("Y");
|
||||||
|
stampAudit(timeline, now);
|
||||||
|
incidentTimelineRepository.save(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스케줄러 스레드는 로그인 세션이 없어 AuditorAware 가 비어있다. CREATED_BY 는 NOT NULL 이므로 직접 채운다.
|
||||||
|
*/
|
||||||
|
private void stampAudit(Auditable entity, LocalDateTime now) {
|
||||||
|
entity.setCreatedBy(AUTHOR_SYSTEM);
|
||||||
|
entity.setCreatedDate(now);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.apistatus;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.yaml.snakeyaml.LoaderOptions;
|
||||||
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Status 자동 탐지 초안 양식({@code classpath:apistatus-draft.yml}).
|
||||||
|
*
|
||||||
|
* <p>자동 생성 공지의 제목·본문·타임라인 문구를 코드가 아닌 설정으로 관리한다.
|
||||||
|
* admin 은 Spring Boot 가 아니라 yml 자동 바인딩이 없으므로 SnakeYAML 로 직접 읽는다
|
||||||
|
* ({@code <context:property-placeholder>} 는 properties 전용이고, 검수 섹션이 리스트라
|
||||||
|
* properties 로는 인덱스 키로 흩어진다).</p>
|
||||||
|
*
|
||||||
|
* <p>파일이 없거나 파싱에 실패하면 {@link #DEFAULTS} 로 동작한다 - 문구 설정 문제로
|
||||||
|
* 장애 탐지 자체가 멈추면 안 되기 때문이다. 개별 키가 비어도 같은 이유로 기본값으로 떨어진다.</p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ApiStatusDraftTemplate {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ApiStatusDraftTemplate.class);
|
||||||
|
|
||||||
|
private static final String RESOURCE = "apistatus-draft.yml";
|
||||||
|
|
||||||
|
// ---- 키 ----
|
||||||
|
public static final String GW_LABEL = "gw-label";
|
||||||
|
|
||||||
|
public static final String TITLE_SINGLE = "title.single";
|
||||||
|
public static final String TITLE_MULTI = "title.multi";
|
||||||
|
public static final String TITLE_HIDDEN_ONLY = "title.hidden-only";
|
||||||
|
|
||||||
|
public static final String BODY_LEAD_PUBLISH = "body.lead-publish";
|
||||||
|
public static final String BODY_LEAD_DRAFT = "body.lead-draft";
|
||||||
|
public static final String BODY_AFFECTED_HEADING = "body.affected-heading";
|
||||||
|
public static final String BODY_FILLED_TEXT = "body.filled-text";
|
||||||
|
public static final String BODY_DRAFT_TEXT = "body.draft-text";
|
||||||
|
|
||||||
|
public static final String TL_DETECTED = "timeline.detected";
|
||||||
|
public static final String TL_API_ADDED = "timeline.api-added";
|
||||||
|
public static final String TL_REDETECTED = "timeline.redetected";
|
||||||
|
public static final String TL_ESCALATED = "timeline.escalated";
|
||||||
|
public static final String TL_RECOVERED_ALL = "timeline.recovered-all";
|
||||||
|
public static final String TL_RECOVERED_PART = "timeline.recovered-part";
|
||||||
|
|
||||||
|
/** yml 을 못 읽었을 때 쓰는 기본 문구 (기존 하드코딩과 동일) */
|
||||||
|
private static final Map<String, String> DEFAULTS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Map<String, String> defaults = new LinkedHashMap<>();
|
||||||
|
defaults.put(GW_LABEL, "GW 인터페이스 {count}건");
|
||||||
|
defaults.put(TITLE_SINGLE, "[자동감지] {first} API {keyword}");
|
||||||
|
defaults.put(TITLE_MULTI, "[자동감지] {first} 외 {rest}종 API {keyword}");
|
||||||
|
defaults.put(TITLE_HIDDEN_ONLY, "[자동감지] {gw} {keyword}");
|
||||||
|
defaults.put(BODY_LEAD_PUBLISH, "<p>{summary} 로 자동 감지된 장애입니다. 상세 내용은 확인 후 갱신될 수 있습니다.</p>");
|
||||||
|
defaults.put(BODY_LEAD_DRAFT, "<p>{summary} 로 자동 감지된 장애입니다. 관리자 검수 후 정식 게시됩니다.</p>");
|
||||||
|
defaults.put(BODY_AFFECTED_HEADING, "영향 API");
|
||||||
|
defaults.put(BODY_FILLED_TEXT, "확인 중입니다.");
|
||||||
|
defaults.put(BODY_DRAFT_TEXT, "[작성 필요] {hint}");
|
||||||
|
defaults.put(TL_DETECTED, "{summary} 자동 감지 ({event})\n영향 API: {affected}");
|
||||||
|
defaults.put(TL_API_ADDED, "영향 API 추가 감지 ({event})\n{affected}");
|
||||||
|
defaults.put(TL_REDETECTED, "재탐지 신호 수신 ({event})");
|
||||||
|
defaults.put(TL_ESCALATED, "지연에서 장애로 격상 ({event})");
|
||||||
|
defaults.put(TL_RECOVERED_ALL, "전체 복구 확인 ({event})\n{affected}");
|
||||||
|
defaults.put(TL_RECOVERED_PART, "일부 복구 확인 ({event})\n{affected}");
|
||||||
|
DEFAULTS = Collections.unmodifiableMap(defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 관리자가 검수 시 채워 넣을 항목 */
|
||||||
|
public static final class Section {
|
||||||
|
private final String title;
|
||||||
|
private final String hint;
|
||||||
|
|
||||||
|
public Section(String title, String hint) {
|
||||||
|
this.title = title;
|
||||||
|
this.hint = hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHint() {
|
||||||
|
return hint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final List<Section> DEFAULT_SECTIONS = Collections.unmodifiableList(new ArrayList<Section>() {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
{
|
||||||
|
add(new Section("발생 원인", "예) 백엔드 DB 커넥션 풀 고갈로 응답 지연 발생"));
|
||||||
|
add(new Section("영향 범위", "예) 조회 API 전 구간 응답 실패, 등록/수정은 정상"));
|
||||||
|
add(new Section("조치 내용", "예) 커넥션 풀 증설 및 장애 인스턴스 격리 완료"));
|
||||||
|
add(new Section("예상 복구 시간", "예) 복구 완료 시 실제 시각으로 갱신"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 점(.) 으로 평탄화한 문구 맵 */
|
||||||
|
private Map<String, String> texts = DEFAULTS;
|
||||||
|
private List<Section> sections = DEFAULT_SECTIONS;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void load() {
|
||||||
|
try (InputStream in = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||||
|
if (in == null) {
|
||||||
|
log.info("{} 없음 - 기본 초안 양식 사용", RESOURCE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SafeConstructor - 임의 클래스 생성(CVE-2022-1471 계열)을 막고 스칼라/맵/리스트만 읽는다
|
||||||
|
Object root = new Yaml(new SafeConstructor(new LoaderOptions())).load(in);
|
||||||
|
Map<String, Object> draft = asMap(asMap(root).get("draft"));
|
||||||
|
if (draft.isEmpty()) {
|
||||||
|
log.warn("{} 에 draft 섹션이 없음 - 기본 초안 양식 사용", RESOURCE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> loaded = new LinkedHashMap<>(DEFAULTS);
|
||||||
|
flatten("", draft, loaded);
|
||||||
|
this.texts = loaded;
|
||||||
|
this.sections = readSections(draft);
|
||||||
|
log.info("초안 양식 로딩 완료: {} (섹션 {}개)", RESOURCE, sections.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 문구 설정 문제로 장애 탐지가 멈추면 안 된다
|
||||||
|
log.warn("{} 로딩 실패 - 기본 초안 양식 사용", RESOURCE, e);
|
||||||
|
this.texts = DEFAULTS;
|
||||||
|
this.sections = DEFAULT_SECTIONS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문구 조회 + 치환. {@code args} 는 {키, 값, 키, 값...} 순으로 넘긴다.
|
||||||
|
* 정의되지 않은 치환자는 그대로 남겨 어떤 키가 빠졌는지 화면에서 드러나게 한다.
|
||||||
|
*/
|
||||||
|
public String text(String key, String... args) {
|
||||||
|
String template = texts.get(key);
|
||||||
|
if (template == null) {
|
||||||
|
template = DEFAULTS.get(key);
|
||||||
|
}
|
||||||
|
if (template == null) {
|
||||||
|
log.warn("정의되지 않은 초안 문구 키: {}", key);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String result = template;
|
||||||
|
for (int i = 0; i + 1 < args.length; i += 2) {
|
||||||
|
result = result.replace("{" + args[i] + "}", args[i + 1] == null ? "" : args[i + 1]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Section> getSections() {
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────── 내부 헬퍼 ──────────────
|
||||||
|
|
||||||
|
/** 중첩 맵을 {@code body.lead-draft} 형태의 평탄 키로 편다. 리스트(sections)는 별도로 읽는다. */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void flatten(String prefix, Map<String, Object> source, Map<String, String> target) {
|
||||||
|
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||||
|
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof Map) {
|
||||||
|
flatten(key, (Map<String, Object>) value, target);
|
||||||
|
} else if (value != null && !(value instanceof List)) {
|
||||||
|
target.put(key, String.valueOf(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Section> readSections(Map<String, Object> draft) {
|
||||||
|
Object body = asMap(draft.get("body")).get("sections");
|
||||||
|
if (!(body instanceof List)) {
|
||||||
|
return DEFAULT_SECTIONS;
|
||||||
|
}
|
||||||
|
List<Section> result = new ArrayList<>();
|
||||||
|
for (Object item : (List<?>) body) {
|
||||||
|
Map<String, Object> section = asMap(item);
|
||||||
|
Object title = section.get("title");
|
||||||
|
if (title == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object hint = section.get("hint");
|
||||||
|
result.add(new Section(String.valueOf(title), hint == null ? "" : String.valueOf(hint)));
|
||||||
|
}
|
||||||
|
return result.isEmpty() ? DEFAULT_SECTIONS : Collections.unmodifiableList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> asMap(Object value) {
|
||||||
|
return value instanceof Map ? (Map<String, Object>) value : Collections.<String, Object>emptyMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,11 @@ import java.util.List;
|
|||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
import com.eactive.eai.data.jpa.BaseRepository;
|
import com.eactive.eai.data.jpa.BaseRepository;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiErrorRateRow;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiNameRow;
|
||||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
|
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.data.entity.onl.djb.apistatus.ApiStatusEvent;
|
||||||
|
|
||||||
@@ -24,7 +28,8 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
|||||||
@Query(nativeQuery = true, value =
|
@Query(nativeQuery = true, value =
|
||||||
" SELECT EAISVCNAME"
|
" SELECT EAISVCNAME"
|
||||||
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
|
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
|
||||||
+ " , PREV_STATUS_CODE "
|
// 따옴표 없는 PREV_STATUS_CODE 는 Oracle 이 대문자로 올려 getPrevStatusCode() 와 매칭되지 않는다 (null 반환)
|
||||||
|
+ " , PREV_STATUS_CODE AS \"prevStatusCode\""
|
||||||
+ " , EVENT"
|
+ " , EVENT"
|
||||||
+ " FROM ("
|
+ " FROM ("
|
||||||
+ " SELECT EAISVCNAME"
|
+ " SELECT EAISVCNAME"
|
||||||
@@ -39,14 +44,17 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
|||||||
+ " FROM ("
|
+ " FROM ("
|
||||||
+ " SELECT A.EAISVCNAME"
|
+ " SELECT A.EAISVCNAME"
|
||||||
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
|
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
|
||||||
|
// 점검(KIND='MAINTENANCE')만 CTRL_YN 대상이다. KIND 를 안 걸면 관리자가 시작~종료 시각을
|
||||||
|
// 넣어 등록한 장애 공지까지 '점검중'으로 오판해 CONTROL_START 가 발생한다.
|
||||||
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
|
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
|
||||||
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
|
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
|
||||||
|
+ " AND Y.KIND = 'MAINTENANCE'"
|
||||||
+ " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) "
|
+ " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) "
|
||||||
+ " AND A.EAISVCNAME = X.API_ID),'N') AS CTRL_YN "
|
+ " AND A.EAISVCNAME = X.API_ID),'N') AS CTRL_YN "
|
||||||
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
|
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
|
||||||
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
|
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL >= :errorRate THEN 'Y'"
|
||||||
+ " ELSE 'N' END"
|
+ " ELSE 'N' END"
|
||||||
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
|
+ " FROM (SELECT NVL(SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT),0) AS TOTAL"
|
||||||
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
|
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
|
||||||
+ " FROM API_STATS_MINUTE"
|
+ " FROM API_STATS_MINUTE"
|
||||||
+ " WHERE A.EAISVCNAME = API_NAME"
|
+ " WHERE A.EAISVCNAME = API_NAME"
|
||||||
@@ -54,9 +62,9 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
|||||||
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:errorRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
|
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:errorRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
|
||||||
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS ERR_YN"
|
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS ERR_YN"
|
||||||
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
|
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
|
||||||
+ " WHEN RESP_SUM / TOTAL > :delayAvgRespTime THEN 'Y'"
|
+ " WHEN RESP_SUM / TOTAL >= :delayAvgRespTime THEN 'Y'"
|
||||||
+ " ELSE 'N' END"
|
+ " ELSE 'N' END"
|
||||||
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
|
+ " FROM (SELECT NVL(SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT),0) AS TOTAL"
|
||||||
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
|
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
|
||||||
+ " , SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME) AS RESP_SUM"
|
+ " , SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME) AS RESP_SUM"
|
||||||
+ " FROM API_STATS_MINUTE"
|
+ " FROM API_STATS_MINUTE"
|
||||||
@@ -73,4 +81,88 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
|||||||
@Param("delayRangeMinute") int delayRangeMinute,
|
@Param("delayRangeMinute") int delayRangeMinute,
|
||||||
@Param("delayAvgRespTime") int delayAvgRespTime
|
@Param("delayAvgRespTime") int delayAvgRespTime
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 판정 구간에 트래픽이 있는 API 의 오류율·지연 판정값. 개발 모드(-Deai.systemmode=D) 진단 로그 전용.
|
||||||
|
*
|
||||||
|
* <p>{@code findApiStatusEvents} 는 임계값을 넘고 <b>상태 전이까지 일어난</b> API 만 돌려주므로
|
||||||
|
* "오류는 나는데 왜 장애로 안 잡히나", "느린데 왜 지연이 아닌가" 를 볼 수 없다. 이 쿼리는
|
||||||
|
* 임계값·현재 상태와 무관하게 집계값을 그대로 보여준다.</p>
|
||||||
|
*
|
||||||
|
* <p>오류가 0건이어도 지연 판정값은 확인해야 하므로 오류 발생 여부로 거르지 않는다.</p>
|
||||||
|
*
|
||||||
|
* <p>컬럼 alias 는 반드시 <b>따옴표로 감싼 camelCase</b> 로 둔다. 인터페이스 프로젝션은 tuple
|
||||||
|
* alias 를 getter 프로퍼티명으로 찾는데, 따옴표가 없으면 Oracle 이 alias 를 대문자로 올려
|
||||||
|
* {@code STATUS_CODE} 와 {@code statusCode} 가 매칭되지 않아 <b>조용히 null</b> 이 된다
|
||||||
|
* (단일 단어는 대소문자만 달라 우연히 매칭된다).</p>
|
||||||
|
*/
|
||||||
|
@Query(nativeQuery = true, value =
|
||||||
|
" SELECT N.API_NAME AS \"eaisvcname\""
|
||||||
|
+ " , NVL(B.STATUS_CODE,'N') AS \"statusCode\""
|
||||||
|
+ " , NVL(S.TOTAL,0) AS \"total\""
|
||||||
|
+ " , NVL(S.SUCCESS,0) AS \"success\""
|
||||||
|
+ " , CASE WHEN NVL(S.TOTAL,0) > 0 THEN ROUND((S.TOTAL - S.SUCCESS) * 100 / S.TOTAL, 2) END AS \"errorRate\""
|
||||||
|
+ " , CASE WHEN NVL(S.TOTAL,0) > 0 THEN ROUND(S.RESP_SUM / S.TOTAL, 2) END AS \"avgRespTime\""
|
||||||
|
+ " , NVL(D.TOTAL,0) AS \"delayTotal\""
|
||||||
|
+ " , CASE WHEN NVL(D.TOTAL,0) > 0 THEN ROUND(D.RESP_SUM / D.TOTAL, 2) END AS \"delayAvgRespTime\""
|
||||||
|
// 두 창 중 넓은 쪽으로 대상 API 를 먼저 뽑는다. 오류 창만 기준으로 잡으면 오류 창에 거래가
|
||||||
|
// 없을 때 지연 판정값까지 통째로 사라진다 (창 길이가 서로 다르다)
|
||||||
|
+ " FROM (SELECT DISTINCT API_NAME"
|
||||||
|
+ " FROM API_STATS_MINUTE"
|
||||||
|
+ " WHERE TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
|
||||||
|
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(GREATEST(:errorRangeMinute, :delayRangeMinute),'MINUTE'),'YYYYMMDDHH24MI')"
|
||||||
|
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI')) N"
|
||||||
|
+ " LEFT OUTER JOIN (SELECT API_NAME"
|
||||||
|
+ " , NVL(SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT),0) AS TOTAL"
|
||||||
|
+ " , NVL(SUM(SUCCESS_CNT),0) AS SUCCESS"
|
||||||
|
+ " , NVL(SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME),0) AS RESP_SUM"
|
||||||
|
+ " FROM API_STATS_MINUTE"
|
||||||
|
+ " WHERE TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
|
||||||
|
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:errorRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
|
||||||
|
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI')"
|
||||||
|
+ " GROUP BY API_NAME) S ON N.API_NAME = S.API_NAME"
|
||||||
|
// 지연은 판정 구간이 따로다(delayRangeMinute). 오류 구간 평균으로 비교하면 실제 판정과 어긋난다
|
||||||
|
+ " LEFT OUTER JOIN (SELECT API_NAME"
|
||||||
|
+ " , NVL(SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT),0) AS TOTAL"
|
||||||
|
+ " , NVL(SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME),0) AS RESP_SUM"
|
||||||
|
+ " FROM API_STATS_MINUTE"
|
||||||
|
+ " WHERE TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
|
||||||
|
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:delayRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
|
||||||
|
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI')"
|
||||||
|
+ " GROUP BY API_NAME) D ON N.API_NAME = D.API_NAME"
|
||||||
|
+ " LEFT OUTER JOIN API_STATUS B ON N.API_NAME = B.EAISVCNAME"
|
||||||
|
// 두 창 중 어느 쪽이든 거래가 있으면 찍는다 (오류 0건, 오류 창 무거래 모두 포함)
|
||||||
|
+ " WHERE NVL(S.TOTAL,0) > 0 OR NVL(D.TOTAL,0) > 0"
|
||||||
|
+ " ORDER BY CASE WHEN NVL(S.TOTAL,0) > 0 THEN (S.TOTAL - S.SUCCESS) / S.TOTAL ELSE -1 END DESC, N.API_NAME")
|
||||||
|
List<ApiErrorRateRow> findApiErrorRates(
|
||||||
|
@Param("errorRangeMinute") int errorRangeMinute,
|
||||||
|
@Param("delayRangeMinute") int delayRangeMinute
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EAI 서비스명 조회. 장애/점검 영향 API 의 API_NAME 캐시용.
|
||||||
|
*/
|
||||||
|
@Query(nativeQuery = true, value =
|
||||||
|
" SELECT EAISVCNAME, EAISVCDESC FROM TSEAIHE01 WHERE EAISVCNAME IN (:apiIds)")
|
||||||
|
List<ApiNameRow> findApiNames(@Param("apiIds") Collection<String> apiIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개발자포탈에 게시되는 인터페이스(= API) 만 추린다.
|
||||||
|
*
|
||||||
|
* <p>GW 인터페이스는 전부 탐지 대상이지만 개발자포탈에 노출되는 것은 그 일부다.
|
||||||
|
* 자동 초안(제목·공지 본문·타임라인)에 게시되지 않은 인터페이스 ID 가 그대로 박히면
|
||||||
|
* 고객 화면에 내부 인터페이스가 드러나므로, 여기서 걸러 나머지는 건수로만 표기한다.</p>
|
||||||
|
*
|
||||||
|
* <p>판정 기준은 포털 "오픈 API" 목록과 같은 두 조건이다 - 노출 중인 API 그룹에 편성돼 있고
|
||||||
|
* PTL_API_SPEC_INFO 에 스펙이 있을 것. 역할·소속에 따른 사용자별 공개 범위는 admin 에
|
||||||
|
* 사용자 컨텍스트가 없어 적용하지 않는다(= 포털 화면이 한 번 더 좁힌다).</p>
|
||||||
|
*/
|
||||||
|
@Query(nativeQuery = true, value =
|
||||||
|
" SELECT DISTINCT GA.API_ID"
|
||||||
|
+ " FROM API_GROUP_API GA"
|
||||||
|
+ " JOIN API_GROUP G ON G.ID = GA.API_GROUP_ID"
|
||||||
|
+ " WHERE GA.API_ID IN (:apiIds)"
|
||||||
|
+ " AND G.DISPLAY_YN = '1'"
|
||||||
|
+ " AND EXISTS (SELECT 1 FROM EMSAPP.PTL_API_SPEC_INFO S WHERE S.API_ID = GA.API_ID)")
|
||||||
|
List<String> findPublishedApiIds(@Param("apiIds") Collection<String> apiIds);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.eactive.eai.rms.ext.djb.apistatus;
|
package com.eactive.eai.rms.ext.djb.apistatus;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -13,6 +15,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||||
import com.eactive.eai.rms.data.entity.man.role.Role;
|
import com.eactive.eai.rms.data.entity.man.role.Role;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiErrorRateRow;
|
||||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
|
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
|
||||||
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
|
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
|
||||||
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
||||||
@@ -36,11 +39,18 @@ public class ApiStatusService {
|
|||||||
|
|
||||||
public void updateApiStatus(HashMap<String, String> param) {
|
public void updateApiStatus(HashMap<String, String> param) {
|
||||||
|
|
||||||
|
int errorRate = Integer.parseInt(param.get("errorRate"));
|
||||||
|
int errorRangeMinute = Integer.parseInt(param.get("errorRangeMinute"));
|
||||||
|
int delayRangeMinute = Integer.parseInt(param.get("delayRangeMinute"));
|
||||||
|
int delayAvgRespTime = Integer.parseInt(param.get("delayAvgRespTime"));
|
||||||
|
|
||||||
|
logErrorRates(errorRate, errorRangeMinute, delayRangeMinute, delayAvgRespTime);
|
||||||
|
|
||||||
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
|
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
|
||||||
Integer.parseInt(param.get("errorRate")),
|
errorRate,
|
||||||
Integer.parseInt(param.get("errorRangeMinute")),
|
errorRangeMinute,
|
||||||
Integer.parseInt(param.get("delayRangeMinute")),
|
delayRangeMinute,
|
||||||
Integer.parseInt(param.get("delayAvgRespTime"))
|
delayAvgRespTime
|
||||||
);
|
);
|
||||||
|
|
||||||
HashMap<String, List<String>> eventMap = new HashMap<>();
|
HashMap<String, List<String>> eventMap = new HashMap<>();
|
||||||
@@ -88,6 +98,85 @@ public class ApiStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 판정 구간에 거래가 있는 API 의 오류율·지연 판정값을 debug 로그로 남긴다.
|
||||||
|
* 개발 모드(-Deai.systemmode=D) 에서만 동작한다.
|
||||||
|
*
|
||||||
|
* <p>상태 전이 조건을 못 넘긴 API 는 {@code findApiStatusEvents} 결과에 나오지 않아
|
||||||
|
* "오류는 나는데 장애로 안 잡힌다", "느린데 지연이 아니다" 를 로그로 확인할 수 없다.
|
||||||
|
* 임계값·현재 상태와 무관하게 집계값을 그대로 찍어 판정 결과와 대조할 수 있게 한다.</p>
|
||||||
|
*
|
||||||
|
* <p>오류 0건이어도 지연 판정값은 봐야 하므로 거래만 있으면 무조건 찍는다.</p>
|
||||||
|
*
|
||||||
|
* <p>진단 목적이므로 조회 실패가 상태 판정 자체를 막아서는 안 된다 - 예외는 삼킨다.</p>
|
||||||
|
*/
|
||||||
|
private void logErrorRates(int errorRate, int errorRangeMinute, int delayRangeMinute, int delayAvgRespTime) {
|
||||||
|
if (!isDevMode() || !log.isDebugEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<ApiErrorRateRow> rows = apiStatusRepository.findApiErrorRates(errorRangeMinute, delayRangeMinute);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
log.debug("상태 판정 진단: 최근 {}분(오류)/{}분(지연) 거래 발생 API 없음",
|
||||||
|
errorRangeMinute, delayRangeMinute);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("상태 판정 진단: 장애 임계 ").append(errorRate).append("% 초과(최근 ").append(errorRangeMinute)
|
||||||
|
.append("분), 지연 임계 ").append(delayAvgRespTime).append("ms 초과(최근 ").append(delayRangeMinute)
|
||||||
|
.append("분), 대상 ").append(rows.size()).append("건");
|
||||||
|
|
||||||
|
for (ApiErrorRateRow row : rows) {
|
||||||
|
sb.append("\n - ").append(row.getEaisvcname())
|
||||||
|
.append(" 현재상태=").append(row.getStatusCode())
|
||||||
|
.append("\n 오류율=").append(row.getErrorRate()).append("%")
|
||||||
|
.append(" (임계 대비 ").append(ratioOf(row.getErrorRate(), errorRate)).append("%)")
|
||||||
|
.append(", 총 ").append(row.getTotal()).append("건, 성공 ").append(row.getSuccess()).append("건")
|
||||||
|
.append(" → ").append(thresholdMark(row.getErrorRate(), errorRate))
|
||||||
|
.append("\n 평균응답=").append(row.getDelayAvgRespTime()).append("ms")
|
||||||
|
.append(" (임계 대비 ").append(ratioOf(row.getDelayAvgRespTime(), delayAvgRespTime)).append("%)")
|
||||||
|
.append(", 총 ").append(row.getDelayTotal()).append("건")
|
||||||
|
.append(" → ").append(thresholdMark(row.getDelayAvgRespTime(), delayAvgRespTime));
|
||||||
|
}
|
||||||
|
log.debug(sb.toString());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("상태 판정 진단 로그 생성 실패", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 임계값 대비 비율(%). 100% 면 임계값과 같은 값이고, 판정은 '초과'(>) 라 100% 는 아직 안 걸린다.
|
||||||
|
*/
|
||||||
|
private String ratioOf(BigDecimal value, int threshold) {
|
||||||
|
if (value == null || threshold <= 0) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return value.multiply(BigDecimal.valueOf(100))
|
||||||
|
.divide(BigDecimal.valueOf(threshold), 2, RoundingMode.HALF_UP)
|
||||||
|
.toPlainString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 임계 초과 여부 표시. 판정 조건이 '초과'(>) 이므로 임계값과 같은 값은 초과가 아니다.
|
||||||
|
*/
|
||||||
|
private String thresholdMark(BigDecimal value, int threshold) {
|
||||||
|
if (value == null) {
|
||||||
|
return "데이터없음";
|
||||||
|
}
|
||||||
|
return value.compareTo(BigDecimal.valueOf(threshold)) > 0 ? "임계초과" : "임계이하";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개발 모드 여부. -Deai.systemmode=D 로 기동한 경우에만 진단 로그를 남긴다.
|
||||||
|
*/
|
||||||
|
private boolean isDevMode() {
|
||||||
|
return "D".equalsIgnoreCase(System.getProperty("eai.systemmode", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private String resolveStatusCode(String event) {
|
private String resolveStatusCode(String event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case "CONTROL_START": return "C"; //점검
|
case "CONTROL_START": return "C"; //점검
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.common;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class DjbProperty {
|
||||||
|
|
||||||
|
// SMS 2차 인증
|
||||||
|
|
||||||
|
@DjbPropertyValue(
|
||||||
|
key = "djb.sms_auth.enabled",
|
||||||
|
defaultValue = "true",
|
||||||
|
description = "SMS 2차 인증 사용 여부. true 시 휴대폰번호 없는 사용자는 ID/PW 로그인 불가")
|
||||||
|
private boolean smsAuthEnabled = false;
|
||||||
|
|
||||||
|
@DjbPropertyValue(
|
||||||
|
key = "djb.sms_auth.mode",
|
||||||
|
defaultValue = "real",
|
||||||
|
description = "인증번호 생성 모드: real(랜덤), hhmm00(현재시각+00), fixed(고정값)")
|
||||||
|
@Pattern(regexp = "^(real|hhmm00|fixed)$", message = "djb.sms_auth.mode는 real, hhmm00, fixed 중 하나여야 합니다.")
|
||||||
|
private String smsAuthMode = "real";
|
||||||
|
|
||||||
|
@DjbPropertyValue(
|
||||||
|
key = "djb.sms_auth.fixed_value",
|
||||||
|
defaultValue = "654321",
|
||||||
|
description = "mode가 fixed일 때 사용할 고정 인증번호 (6자리 숫자)")
|
||||||
|
@Pattern(regexp = "^\\d{6}$", message = "djb.sms_auth.fixed_value는 6자리 숫자여야 합니다.")
|
||||||
|
private String smsAuthFixedValue = "654321";
|
||||||
|
|
||||||
|
|
||||||
|
@DjbPropertyValue(
|
||||||
|
key = "djb.api.allow_ip_list",
|
||||||
|
description = "API 접근 허용 IP 목록 (콤마(,)로 구분된 IP들, 예: 192.168.0.1, 192.168.1.*,127.*.*.*",
|
||||||
|
defaultValue = "10.15.106.*, 10.14.8.*, 10.15.8.*, 192.168.240.*, 127.0.0.1, 192.168.132.*, 192.168.131.*"
|
||||||
|
)
|
||||||
|
private String apiAllowIpList = "10.15.106.*, 10.14.8.*, 10.15.8.*, 192.168.240.*, 127.0.0.1, 192.168.132.*, 192.168.131.*";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public String toJsonString(boolean isPretty) {
|
||||||
|
return isPretty
|
||||||
|
? new GsonBuilder().setPrettyPrinting().create().toJson(this)
|
||||||
|
: new Gson().toJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 기본 프로퍼티 목록을 콘솔로 출력 (가이드용)
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("| Key | Default Value | Description |");
|
||||||
|
System.out.println("|-----|---------------|-------------|");
|
||||||
|
|
||||||
|
for (java.lang.reflect.Field field : DjbProperty.class.getDeclaredFields()) {
|
||||||
|
DjbPropertyValue annotation = field.getAnnotation(DjbPropertyValue.class);
|
||||||
|
if (annotation != null) {
|
||||||
|
String key = annotation.key();
|
||||||
|
String defaultValue = annotation.defaultValue();
|
||||||
|
String description = annotation.description();
|
||||||
|
System.out.printf("| %s | %s | %s |%n", key, defaultValue, description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.common;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DjbProperty 싱글톤 홀더.
|
||||||
|
*
|
||||||
|
* <p>모든 곳에서 {@code DjbPropertyHolder.get()}으로 현재 DjbProperty에 접근합니다.
|
||||||
|
* eapim-admin에서 초기화 시 {@code initialize()}를 호출하고,
|
||||||
|
* MonitoringContext.refresh() 시 {@code reload()}를 호출하여 값을 갱신합니다.</p>
|
||||||
|
*
|
||||||
|
* <p>사용 예시:</p>
|
||||||
|
* <pre>
|
||||||
|
* // 값 접근
|
||||||
|
* String url = DjbPropertyHolder.get().getUmsHostUrl();
|
||||||
|
*
|
||||||
|
* // 초기화 (eapim-admin에서)
|
||||||
|
* DjbPropertyInjector injector = new DjbPropertyInjector(service, "Monitoring");
|
||||||
|
* DjbPropertyHolder.initialize(injector::inject);
|
||||||
|
*
|
||||||
|
* // reload (MonitoringContext.refresh() 시)
|
||||||
|
* DjbPropertyHolder.reload();
|
||||||
|
*
|
||||||
|
* // 테스트용
|
||||||
|
* DjbPropertyHolder.setForTest(mockProperty);
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class DjbPropertyHolder {
|
||||||
|
|
||||||
|
private static volatile DjbProperty instance = new DjbProperty();
|
||||||
|
private static volatile Function<DjbProperty, DjbProperty> injector;
|
||||||
|
|
||||||
|
private DjbPropertyHolder() {
|
||||||
|
// 유틸리티 클래스
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 DjbProperty 인스턴스를 반환합니다.
|
||||||
|
*/
|
||||||
|
public static DjbProperty get() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 초기화 함수를 등록하고 최초 로딩을 수행합니다.
|
||||||
|
* eapim-admin 부팅 시 MonitoringContextImpl.init()에서 호출됩니다.
|
||||||
|
*
|
||||||
|
* @param injectorFn DjbProperty를 DB에서 로딩하는 함수 (DjbPropertyInjector::inject)
|
||||||
|
*/
|
||||||
|
public static void initialize(Function<DjbProperty, DjbProperty> injectorFn) {
|
||||||
|
injector = injectorFn;
|
||||||
|
reload();
|
||||||
|
log.info("DjbPropertyHolder 초기화 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DB에서 프로퍼티를 다시 로딩합니다.
|
||||||
|
* MonitoringContext.refresh() 호출 시 함께 호출됩니다.
|
||||||
|
*/
|
||||||
|
public static void reload() {
|
||||||
|
if (injector != null) {
|
||||||
|
try {
|
||||||
|
instance = injector.apply(new DjbProperty());
|
||||||
|
log.info("DjbProperty reload 완료");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("DjbProperty reload 실패: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("DjbPropertyHolder: injector가 설정되지 않음 (테스트 환경일 수 있음)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테스트용 - 직접 인스턴스를 설정합니다.
|
||||||
|
*/
|
||||||
|
public static void setForTest(DjbProperty testProperty) {
|
||||||
|
instance = testProperty;
|
||||||
|
log.debug("DjbProperty 테스트 인스턴스 설정");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테스트용 - 기본값으로 초기화합니다.
|
||||||
|
*/
|
||||||
|
public static void resetForTest() {
|
||||||
|
instance = new DjbProperty();
|
||||||
|
injector = null;
|
||||||
|
log.debug("DjbPropertyHolder 테스트 초기화");
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-22
@@ -1,10 +1,8 @@
|
|||||||
package com.eactive.ext.kjb.util;
|
package com.eactive.eai.rms.ext.djb.common;
|
||||||
|
|
||||||
import com.eactive.eai.rms.data.entity.man.monitoringProperty.MonitoringProperty;
|
import com.eactive.eai.rms.data.entity.man.monitoringProperty.MonitoringProperty;
|
||||||
import com.eactive.eai.rms.data.entity.man.monitoringProperty.MonitoringPropertyId;
|
import com.eactive.eai.rms.data.entity.man.monitoringProperty.MonitoringPropertyId;
|
||||||
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
|
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
|
||||||
import com.eactive.ext.kjb.common.KjbProperty;
|
|
||||||
import com.eactive.ext.kjb.common.KjbPropertyValue;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
@@ -12,52 +10,49 @@ import java.lang.reflect.Field;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class KjbPropertyInjector {
|
public class DjbPropertyInjector {
|
||||||
private final MonitoringPropertyService monitoringPropertyService;
|
private final MonitoringPropertyService monitoringPropertyService;
|
||||||
private final String groupId;
|
private final String groupId;
|
||||||
|
|
||||||
|
|
||||||
public KjbPropertyInjector(MonitoringPropertyService monitoringPropertyService, String groupId) {
|
public DjbPropertyInjector(MonitoringPropertyService monitoringPropertyService, String groupId) {
|
||||||
this.monitoringPropertyService = monitoringPropertyService;
|
this.monitoringPropertyService = monitoringPropertyService;
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KjbProperty의 URL 값들이 설정되어 있는지 확인
|
* DjbProperty의 URL 값들이 설정되어 있는지 확인
|
||||||
* @param property 검사할 KjbProperty 객체
|
* @param property 검사할 DjbProperty 객체
|
||||||
* @return URL 값들이 모두 설정되어 있으면 true
|
* @return URL 값들이 모두 설정되어 있으면 true
|
||||||
*/
|
*/
|
||||||
public boolean isUrlConfigured(KjbProperty property) {
|
public boolean isUrlConfigured(DjbProperty property) {
|
||||||
if (property == null) return false;
|
if (property == null) return false;
|
||||||
|
|
||||||
// 주요 URL 값들 중 하나라도 설정되어 있으면 true
|
return StringUtils.isNotEmpty(property.getApiAllowIpList());
|
||||||
return StringUtils.isNotEmpty(property.getEaiBatchUrl())
|
|
||||||
|| StringUtils.isNotEmpty(property.getUmsHostUrl())
|
|
||||||
|| StringUtils.isNotEmpty(property.getObpUrl());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL 값이 비어있을 경우 DB에서 재로딩 시도
|
* URL 값이 비어있을 경우 DB에서 재로딩 시도
|
||||||
* @param property 기존 KjbProperty 객체
|
* @param property 기존 DjbProperty 객체
|
||||||
* @return 재로딩된 KjbProperty 객체 (재로딩 실패 시 기존 객체 반환)
|
* @return 재로딩된 DjbProperty 객체 (재로딩 실패 시 기존 객체 반환)
|
||||||
*/
|
*/
|
||||||
public KjbProperty reloadIfUrlEmpty(KjbProperty property) {
|
public DjbProperty reloadIfUrlEmpty(DjbProperty property) {
|
||||||
if (property == null) {
|
if (property == null) {
|
||||||
return inject(new KjbProperty());
|
return inject(new DjbProperty());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isUrlConfigured(property)) {
|
if (!isUrlConfigured(property)) {
|
||||||
log.info("KjbProperty URL 값이 비어있어 DB에서 재로딩 시도");
|
log.info("DjbProperty URL 값이 비어있어 DB에서 재로딩 시도");
|
||||||
try {
|
try {
|
||||||
KjbProperty reloaded = inject(new KjbProperty());
|
DjbProperty reloaded = inject(new DjbProperty());
|
||||||
if (isUrlConfigured(reloaded)) {
|
if (isUrlConfigured(reloaded)) {
|
||||||
log.info("KjbProperty 재로딩 성공");
|
log.info("DjbProperty 재로딩 성공");
|
||||||
return reloaded;
|
return reloaded;
|
||||||
} else {
|
} else {
|
||||||
log.warn("KjbProperty 재로딩 후에도 URL 값이 비어있음");
|
log.warn("DjbProperty 재로딩 후에도 URL 값이 비어있음");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("KjbProperty 재로딩 실패: {}", e.getMessage(), e);
|
log.error("DjbProperty 재로딩 실패: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +63,7 @@ public class KjbPropertyInjector {
|
|||||||
Class<?> clazz = property.getClass();
|
Class<?> clazz = property.getClass();
|
||||||
|
|
||||||
for (Field field : clazz.getDeclaredFields()) {
|
for (Field field : clazz.getDeclaredFields()) {
|
||||||
KjbPropertyValue anno = field.getAnnotation(KjbPropertyValue.class);
|
DjbPropertyValue anno = field.getAnnotation(DjbPropertyValue.class);
|
||||||
|
|
||||||
if (anno != null) {
|
if (anno != null) {
|
||||||
String key = anno.key();
|
String key = anno.key();
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.common;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
public @interface DjbPropertyValue {
|
||||||
|
String key();
|
||||||
|
String defaultValue() default "";
|
||||||
|
String description() default "";
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package com.eactive.eai.rms.ext.djb.job;
|
package com.eactive.eai.rms.ext.djb.job;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.PersistenceContext;
|
import javax.persistence.PersistenceContext;
|
||||||
@@ -28,7 +29,8 @@ import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsHour;
|
|||||||
/**
|
/**
|
||||||
* API 로그 테이블을 시간별 통계로 집계하는 배치 작업
|
* API 로그 테이블을 시간별 통계로 집계하는 배치 작업
|
||||||
* TSEAILGXX → API_STATS_HOUR
|
* TSEAILGXX → API_STATS_HOUR
|
||||||
* 실행 주기: 매시 00:10 (당일 데이터 집계)
|
* 실행 주기: 매시 00:10, 직전 1시간(예: 14:10 실행 시 13:00~13:59:59) 데이터만 집계
|
||||||
|
* (0시대 실행 시 전날 23:00~23:59:59 집계)
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class ApiStatsHourlyAggregationJob implements Job {
|
public class ApiStatsHourlyAggregationJob implements Job {
|
||||||
@@ -59,11 +61,9 @@ public class ApiStatsHourlyAggregationJob implements Job {
|
|||||||
DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
|
DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
|
||||||
DataSourceContextHolder.setDataSourceType(dataType);
|
DataSourceContextHolder.setDataSourceType(dataType);
|
||||||
try {
|
try {
|
||||||
LocalDate targetDate = LocalDate.now();
|
// 직전 1시간(예: 14:10 실행 -> 13:00~13:59:59). 0시대 실행 시 자동으로 전날 23시로 롤오버됨
|
||||||
if (LocalTime.now().isBefore(LocalTime.of(1, 0))) {
|
LocalDateTime targetHour = LocalDateTime.now().minusHours(1).truncatedTo(ChronoUnit.HOURS);
|
||||||
targetDate = targetDate.minusDays(1);
|
selfJob.executeManual(targetHour);
|
||||||
}
|
|
||||||
selfJob.executeManual(targetDate);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new JobExecutionException(e);
|
throw new JobExecutionException(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -72,22 +72,38 @@ public class ApiStatsHourlyAggregationJob implements Job {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 수동 실행 메서드 (날짜 지정)
|
* 수동 실행 메서드 (날짜 지정, 하루 전체 재집계 - 관리자 화면 백필용)
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public int executeManual(LocalDate targetDate) {
|
public int executeManual(LocalDate targetDate) {
|
||||||
|
LocalDateTime rangeStart = targetDate.atStartOfDay();
|
||||||
|
return aggregate(rangeStart, rangeStart.plusDays(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수동/스케줄 실행 메서드 (시각 지정, 해당 시각이 속한 1시간만 집계)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public int executeManual(LocalDateTime targetHour) {
|
||||||
|
LocalDateTime rangeStart = targetHour.truncatedTo(ChronoUnit.HOURS);
|
||||||
|
return aggregate(rangeStart, rangeStart.plusHours(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int aggregate(LocalDateTime rangeStart, LocalDateTime rangeEnd) {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
log.info("=== 시간별 통계 집계 작업 시작 === 대상: {}", targetDate);
|
log.info("=== 시간별 통계 집계 작업 시작 === 대상: {} ~ {}", rangeStart, rangeEnd);
|
||||||
|
|
||||||
String searchDate = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
DateTimeFormatter tsFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||||
String logTableName = CommonUtil.getLogTable(searchDate, true);
|
String logTableName = CommonUtil.getLogTable(rangeStart.format(DateTimeFormatter.ofPattern("yyyyMMdd")), true);
|
||||||
|
String rangeStartStr = rangeStart.format(tsFormat) + "000";
|
||||||
|
String rangeEndStr = rangeEnd.format(tsFormat) + "000";
|
||||||
|
|
||||||
// 1. 기존 데이터 삭제
|
// 1. 대상 구간(rangeStart ~ rangeEnd)의 기존 데이터만 삭제 (다른 시간대 데이터는 보존)
|
||||||
QApiStatsHour q = QApiStatsHour.apiStatsHour;
|
QApiStatsHour q = QApiStatsHour.apiStatsHour;
|
||||||
long deletedCount = apiStatsHourService.getJPAQueryFactory()
|
long deletedCount = apiStatsHourService.getJPAQueryFactory()
|
||||||
.delete(q)
|
.delete(q)
|
||||||
.where(q.statTime.goe(targetDate.atStartOfDay())
|
.where(q.statTime.goe(rangeStart)
|
||||||
.and(q.statTime.lt(targetDate.plusDays(1).atStartOfDay())))
|
.and(q.statTime.lt(rangeEnd)))
|
||||||
.execute();
|
.execute();
|
||||||
log.info("기존 데이터 삭제: {} 건", deletedCount);
|
log.info("기존 데이터 삭제: {} 건", deletedCount);
|
||||||
|
|
||||||
@@ -122,18 +138,23 @@ public class ApiStatsHourlyAggregationJob implements Job {
|
|||||||
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN GSTATSYSADPTRBZWKGROUPNAME ELSE '' END) AS INBOUND_ADAPTER" +
|
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN GSTATSYSADPTRBZWKGROUPNAME ELSE '' END) AS INBOUND_ADAPTER" +
|
||||||
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN PSVSYSADPTRBZWKGROUPNAME ELSE '' END) AS OUTBOUND_ADAPTER" +
|
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN PSVSYSADPTRBZWKGROUPNAME ELSE '' END) AS OUTBOUND_ADAPTER" +
|
||||||
" , MIN(MSGDPSTYMS) AS DT" +
|
" , MIN(MSGDPSTYMS) AS DT" +
|
||||||
" , MAX(MSGPRCSSYMS) - MIN(MSGDPSTYMS) AS RESP_TIME" +
|
" , ( (TO_DATE(SUBSTR(MAX(MSGPRCSSYMS),1,14),'YYYYMMDDHH24MISS')" +
|
||||||
|
" - TO_DATE(SUBSTR(MIN(MSGPRCSSYMS),1,14),'YYYYMMDDHH24MISS')) * 86400000" +
|
||||||
|
" + TO_NUMBER(SUBSTR(MAX(MSGPRCSSYMS),15,3))" +
|
||||||
|
" - TO_NUMBER(SUBSTR(MIN(MSGPRCSSYMS),15,3))" +
|
||||||
|
" ) AS RESP_TIME" +
|
||||||
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" +
|
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" +
|
||||||
" , MAX(EAIERRCD) AS ERROR_CODE" +
|
" , MAX(EAIERRCD) AS ERROR_CODE" +
|
||||||
" FROM " + logTableName +
|
" FROM " + logTableName +
|
||||||
" WHERE MSGDPSTYMS LIKE :searchDate || '%'" +
|
" WHERE MSGDPSTYMS >= :rangeStart AND MSGDPSTYMS < :rangeEnd" +
|
||||||
" GROUP BY EAISVCSERNO" +
|
" GROUP BY EAISVCSERNO" +
|
||||||
" ) A LEFT OUTER JOIN TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.USEYN = 'Y' " +
|
" ) 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')" +
|
" GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
|
||||||
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER";
|
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER";
|
||||||
|
|
||||||
int savedCount = entityManager.createNativeQuery(sql)
|
int savedCount = entityManager.createNativeQuery(sql)
|
||||||
.setParameter("searchDate", searchDate)
|
.setParameter("rangeStart", rangeStartStr)
|
||||||
|
.setParameter("rangeEnd", rangeEndStr)
|
||||||
.executeUpdate();
|
.executeUpdate();
|
||||||
|
|
||||||
long elapsedTime = System.currentTimeMillis() - startTime;
|
long elapsedTime = System.currentTimeMillis() - startTime;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.eactive.eai.rms.ext.kjb.smsauth;
|
package com.eactive.eai.rms.ext.djb.smsauth;
|
||||||
|
|
||||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||||
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
|
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.eactive.eai.rms.ext.kjb.smsauth;
|
package com.eactive.eai.rms.ext.djb.smsauth;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
+8
-12
@@ -1,4 +1,4 @@
|
|||||||
package com.eactive.eai.rms.ext.kjb.smsauth;
|
package com.eactive.eai.rms.ext.djb.smsauth;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -13,11 +13,9 @@ import org.springframework.stereotype.Service;
|
|||||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||||
import com.eactive.eai.rms.common.login.LoginVo;
|
import com.eactive.eai.rms.common.login.LoginVo;
|
||||||
|
import com.eactive.eai.rms.ext.djb.common.DjbProperty;
|
||||||
|
import com.eactive.eai.rms.ext.djb.common.DjbPropertyHolder;
|
||||||
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
||||||
import com.eactive.ext.kjb.common.KjbProperty;
|
|
||||||
import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
|
||||||
import com.eactive.ext.kjb.common.KjbUmsException;
|
|
||||||
import com.eactive.ext.kjb.ums.KjbUmsService;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -46,13 +44,11 @@ public class SmsAuthService {
|
|||||||
|
|
||||||
private final UmsManager ums;
|
private final UmsManager ums;
|
||||||
|
|
||||||
private static KjbProperty getProperty() {
|
private static DjbProperty getProperty() {
|
||||||
return KjbPropertyHolder.get();
|
return DjbPropertyHolder.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private KjbUmsService getUmsService() {
|
|
||||||
return KjbUmsService.getInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMS 인증이 필요한지 확인
|
* SMS 인증이 필요한지 확인
|
||||||
@@ -86,7 +82,7 @@ public class SmsAuthService {
|
|||||||
* 인증번호 생성
|
* 인증번호 생성
|
||||||
*/
|
*/
|
||||||
public String generateAuthCode() {
|
public String generateAuthCode() {
|
||||||
KjbProperty prop = getProperty();
|
DjbProperty prop = getProperty();
|
||||||
String mode = prop.getSmsAuthMode();
|
String mode = prop.getSmsAuthMode();
|
||||||
|
|
||||||
if (MODE_FIXED.equalsIgnoreCase(mode)) {
|
if (MODE_FIXED.equalsIgnoreCase(mode)) {
|
||||||
@@ -113,7 +109,7 @@ public class SmsAuthService {
|
|||||||
HashMap<String,Object> params = new HashMap<String, Object>();
|
HashMap<String,Object> params = new HashMap<String, Object>();
|
||||||
params.put("authNumber", authCode);
|
params.put("authNumber", authCode);
|
||||||
params.put("userName", userInfo.getUsername());
|
params.put("userName", userInfo.getUsername());
|
||||||
log.debug("\n\n\nDDDDDDDDD" + userInfo.toString());
|
|
||||||
ums.send(userInfo, MessageCode.ADMIN_VERIFICATION_MOBILEPHONE, params);
|
ums.send(userInfo, MessageCode.ADMIN_VERIFICATION_MOBILEPHONE, params);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -61,13 +61,12 @@ public class ApiUseStatsService
|
|||||||
|
|
||||||
|
|
||||||
private Query getDataQuery(ApiStatsSearch search) {
|
private Query getDataQuery(ApiStatsSearch search) {
|
||||||
|
|
||||||
StringBuilder where = buildNativeWhere(search);
|
StringBuilder where = buildNativeWhere(search);
|
||||||
|
|
||||||
String dataSql = "";
|
String dataSql = "";
|
||||||
if ("ORG".equals(search.getSearchType())) {
|
if ("ORG".equals(search.getSearchType())) {
|
||||||
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
|
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
|
||||||
+ ", B.ORGNAME"
|
+ ", NVL(B.ORGNAME, D.ORG_NAME) AS ORGNAME"
|
||||||
+ ", SUM(A.TOTAL_CNT)"
|
+ ", SUM(A.TOTAL_CNT)"
|
||||||
+ ", SUM(A.SUCCESS_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.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
|
||||||
@@ -77,13 +76,41 @@ public class ApiUseStatsService
|
|||||||
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
|
+ ", 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)"
|
+ ", MIN(A.MIN_RESP_TIME)"
|
||||||
+ ", MAX(A.MAX_RESP_TIME)"
|
+ ", MAX(A.MAX_RESP_TIME)"
|
||||||
+ " FROM API_STATS_DAY A"
|
+ " FROM ("
|
||||||
|
+ " SELECT A.STAT_TIME "
|
||||||
|
+ " , A.API_NAME"
|
||||||
|
+ " , A.CLIENT_ID "
|
||||||
|
+ " , A.TOTAL_CNT"
|
||||||
|
+ " , A.SUCCESS_CNT"
|
||||||
|
+ " , A.AVG_RESP_TIME"
|
||||||
|
+ " , A.TIMEOUT_CNT"
|
||||||
|
+ " , A.SYSTEM_ERR_CNT"
|
||||||
|
+ " , A.MIN_RESP_TIME"
|
||||||
|
+ " , A.MAX_RESP_TIME"
|
||||||
|
+ " , CASE WHEN API_NAME LIKE '%1' THEN SUBSTR(INBOUND_ADAPTER,2,3) ELSE SUBSTR(OUTBOUND_ADAPTER,2,3) END AS ORG_CODE"
|
||||||
|
+ " FROM API_STATS_DAY A "
|
||||||
|
+ " WHERE A.STAT_TIME >= :searchStartDate "
|
||||||
|
+ " AND A.STAT_TIME <= :searchEndDate ";
|
||||||
|
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchApiName())) {
|
||||||
|
dataSql += " AND A.API_NAME LIKE :searchApiName ";
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSql += " ) A"
|
||||||
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
|
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
|
||||||
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
|
+ " LEFT OUTER JOIN EMSAPP.PTL_ORG D ON A.ORG_CODE = D.ORG_CODE"
|
||||||
+ where
|
+ " WHERE 1=1 ";
|
||||||
+ " GROUP BY B.ORGNAME"
|
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
|
||||||
|
dataSql += " AND UPPER(NVL(B.ORGNAME, D.ORG_NAME)) LIKE :searchOrgName ";
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSql += " GROUP BY NVL(B.ORGNAME, D.ORG_NAME)"
|
||||||
+ " ORDER BY ORGNAME";
|
+ " ORDER BY ORGNAME";
|
||||||
} else if ("API".equals(search.getSearchType())) {
|
|
||||||
|
}
|
||||||
|
else if ("API".equals(search.getSearchType())) {
|
||||||
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
|
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
|
||||||
+ ", A.API_NAME"
|
+ ", A.API_NAME"
|
||||||
+ ", SUM(A.TOTAL_CNT)"
|
+ ", SUM(A.TOTAL_CNT)"
|
||||||
@@ -97,11 +124,11 @@ public class ApiUseStatsService
|
|||||||
+ ", MAX(A.MAX_RESP_TIME)"
|
+ ", MAX(A.MAX_RESP_TIME)"
|
||||||
+ " FROM API_STATS_DAY A "
|
+ " FROM API_STATS_DAY A "
|
||||||
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
|
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
|
||||||
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
|
|
||||||
+ where
|
+ where
|
||||||
+ " GROUP BY A.API_NAME"
|
+ " GROUP BY A.API_NAME"
|
||||||
+ " ORDER BY API_NAME";
|
+ " ORDER BY API_NAME";
|
||||||
} else if ("DATE".equals(search.getSearchType())) {
|
}
|
||||||
|
else if ("DATE".equals(search.getSearchType())) {
|
||||||
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
|
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
|
||||||
+ ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME "
|
+ ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME "
|
||||||
+ ", SUM(A.TOTAL_CNT)"
|
+ ", SUM(A.TOTAL_CNT)"
|
||||||
@@ -115,23 +142,24 @@ public class ApiUseStatsService
|
|||||||
+ ", MAX(A.MAX_RESP_TIME)"
|
+ ", MAX(A.MAX_RESP_TIME)"
|
||||||
+ " FROM API_STATS_DAY A "
|
+ " FROM API_STATS_DAY A "
|
||||||
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
|
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
|
||||||
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
|
|
||||||
+ where
|
+ where
|
||||||
+ " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')"
|
+ " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')"
|
||||||
+ " ORDER BY STAT_TIME";
|
+ " ORDER BY STAT_TIME";
|
||||||
}
|
}
|
||||||
|
|
||||||
Query dataQuery = entityManager.createNativeQuery(dataSql);
|
Query dataQuery = entityManager.createNativeQuery(dataSql);
|
||||||
if (search.getParsedStartDate() != null) {
|
|
||||||
|
|
||||||
dataQuery.setParameter("searchStartDate", search.getParsedStartDate());
|
dataQuery.setParameter("searchStartDate", search.getParsedStartDate());
|
||||||
dataQuery.setParameter("searchEndDate", search.getParsedEndDate());
|
dataQuery.setParameter("searchEndDate", search.getParsedEndDate());
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchApiName())) {
|
||||||
|
dataQuery.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%");
|
||||||
}
|
}
|
||||||
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
|
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
|
||||||
dataQuery.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%");
|
dataQuery.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%");
|
||||||
}
|
}
|
||||||
if (StringUtils.isNotBlank(search.getSearchApiName())) {
|
|
||||||
dataQuery.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%");
|
|
||||||
}
|
|
||||||
return dataQuery;
|
return dataQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,19 +191,17 @@ public class ApiUseStatsService
|
|||||||
|
|
||||||
calculateDateRange(search);
|
calculateDateRange(search);
|
||||||
|
|
||||||
if (search.getParsedStartDate() != null) {
|
|
||||||
where.append(" AND A.STAT_TIME >= :searchStartDate ");
|
where.append(" AND A.STAT_TIME >= :searchStartDate ");
|
||||||
where.append(" AND A.STAT_TIME <= :searchEndDate ");
|
where.append(" AND A.STAT_TIME <= :searchEndDate ");
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchApiName())) {
|
||||||
|
where.append(" AND A.API_NAME LIKE :searchApiName ");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
|
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
|
||||||
where.append(" AND UPPER(B.ORGNAME) LIKE :searchOrgName ");
|
where.append(" AND UPPER(B.ORGNAME) LIKE :searchOrgName ");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(search.getSearchApiName())) {
|
|
||||||
where.append(" AND UPPER(C.EAISVCDESC) LIKE :searchApiName ");
|
|
||||||
}
|
|
||||||
|
|
||||||
return where;
|
return where;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ public class UmsService {
|
|||||||
|
|
||||||
private static final String SYS_DVCD = "API"; //시스템코드
|
private static final String SYS_DVCD = "API"; //시스템코드
|
||||||
private static final String BZWK_DVCD = "99"; //업무구분코드
|
private static final String BZWK_DVCD = "99"; //업무구분코드
|
||||||
private static final String SUB_BZWK_CD = "9019"; //세부구분코드
|
private static final String BZWK_CD = "999"; //업무코드
|
||||||
private static final String TEAM_DVCD = "00"; //팀구분코드 TODO: 변경해야함
|
private static final String BZWK_DCLS_DVCD = "9019"; //세부구분코드
|
||||||
|
private static final String SUB_BZWK_CD = "999"; //서브업무코드 TODO: 변경할것
|
||||||
|
private static final String TEAM_DVCD = "99"; //팀구분코드 TODO: 변경해야함
|
||||||
|
|
||||||
|
|
||||||
private final MonitoringPropertyService propertyService;
|
private final MonitoringPropertyService propertyService;
|
||||||
@@ -199,33 +201,33 @@ public class UmsService {
|
|||||||
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
|
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
|
||||||
|
|
||||||
}
|
}
|
||||||
else if ("SMS".equals(messageRequest.getMessageType())
|
else if ("SMS".equals(messageRequest.getMessageType())) {
|
||||||
|| "KAKAO".equals(messageRequest.getMessageType())) {
|
|
||||||
|
|
||||||
// Sms 데이터
|
// Sms 데이터
|
||||||
SmsDataVO dataVO = new SmsDataVO();
|
SmsDataVO dataVO = new SmsDataVO();
|
||||||
dataVO.setSnd_dman_dt(jsonData[0]);
|
dataVO.setSnd_dman_dt(jsonData[0]);
|
||||||
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
|
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
|
||||||
dataVO.setSnd_dvcd("EAI");
|
dataVO.setSnd_dvcd("EAI");
|
||||||
dataVO.setMsg_titl(StringUtil.chunkString(messageRequest.getSubject(), 40, false)); // MS949 40byte 초과시 절단
|
dataVO.setSms_msg_titl(StringUtil.chunkString(messageRequest.getSubject(), 40, false)); // MS949 40byte 초과시 절단
|
||||||
dataVO.setMsg_ctnt(messageRequest.getMessage());
|
dataVO.setSms_msg_ctnt(messageRequest.getMessage());
|
||||||
dataVO.setRecvr_no(PhoneNumberUtil.digitsOnly(messageRequest.getPhone()));
|
dataVO.setRecvr_no(PhoneNumberUtil.digitsOnly(messageRequest.getPhone()));
|
||||||
dataVO.setSndn_no("15880079"); //발신번호
|
dataVO.setSndn_no("15880079"); //발신번호
|
||||||
dataVO.setSnd_empno("APIM"); //요청자
|
dataVO.setSnd_empno("APIM"); //요청자
|
||||||
dataVO.setSnd_emp_dprm_cd("132"); //요청부서코드
|
dataVO.setSnd_emp_dprm_cd("132"); //요청부서코드
|
||||||
if ("SMS".equals(messageRequest.getMessageType())) {
|
|
||||||
dataVO.setLtrs_snd_tycd("1"); // 1:SMS 2:LMS/MMS
|
|
||||||
dataVO.setMsg_snd_tycd("0000"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
|
|
||||||
} else if ("KAKAO".equals(messageRequest.getMessageType())) {
|
|
||||||
dataVO.setLtrs_snd_tycd("2"); // 1:SMS 2:LMS/MMS
|
dataVO.setLtrs_snd_tycd("2"); // 1:SMS 2:LMS/MMS
|
||||||
dataVO.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
|
dataVO.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
|
||||||
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key", "")); // 플러스친구아이디
|
//ADMIN으로 시작하면 제주은행 임직원, 나머지는 제주은행 고객용
|
||||||
dataVO.setAlmtk_tmplt_cd(BZWK_DVCD + TEAM_DVCD + SUB_BZWK_CD + "001"); // 템플릿코드
|
if (messageRequest.getMessageCode().name().startsWith("ADMIN")) {
|
||||||
|
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key.intra", "")); // 제주은행 임직원 플러스친구아이디
|
||||||
|
} else {
|
||||||
|
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key.public", "")); // 제주은행 플러스친구아이디
|
||||||
|
}
|
||||||
|
dataVO.setAlmtk_tmplt_cd(BZWK_DVCD + TEAM_DVCD + BZWK_DCLS_DVCD + "001"); // 템플릿코드
|
||||||
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
|
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
|
||||||
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
|
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
|
||||||
}
|
|
||||||
dataVO.setSms_trnm_bzwk_dvcd(BZWK_DVCD); //업무구분코드
|
dataVO.setSms_trnm_bzwk_dvcd(BZWK_DVCD); //업무구분코드
|
||||||
dataVO.setSms_bzwk_dcls_dvcd(SUB_BZWK_CD); //업무세분코드
|
dataVO.setSms_bzwk_dcls_dvcd(BZWK_DCLS_DVCD); //업무세분코드
|
||||||
dataVO.setSms_sys_dvcd("83"); //시스템코드
|
dataVO.setSms_sys_dvcd("83"); //시스템코드
|
||||||
dataVO.setTx_dt(jsonData[0]); //거래일자
|
dataVO.setTx_dt(jsonData[0]); //거래일자
|
||||||
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
|
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
|
||||||
@@ -239,8 +241,8 @@ public class UmsService {
|
|||||||
// 헤더 + 데이터
|
// 헤더 + 데이터
|
||||||
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
|
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
|
||||||
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
|
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
|
||||||
+ " \"ums_if_data_ncnt\":1, \r\n"
|
+ " \"ums_if_data_ccnt\":1, \r\n"
|
||||||
+ " \"ums_if_data\":" + data
|
+ " \"ltrsAlmtkSndLnkdInputDto\":" + data
|
||||||
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
|
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -248,26 +250,26 @@ public class UmsService {
|
|||||||
|
|
||||||
// 이메일 데이터
|
// 이메일 데이터
|
||||||
EmailDataVO dataVO = new EmailDataVO();
|
EmailDataVO dataVO = new EmailDataVO();
|
||||||
dataVO.setUms_evnt_id(""); // UMS이벤트ID(필수)
|
dataVO.setUms_evnt_id(propertyService.getPropertyValue("Monitoring", "djb.ums.email.ums_evnt_id", "")); // UMS이벤트ID(필수)
|
||||||
dataVO.setSnd_dman_dt(jsonData[0]);
|
dataVO.setSnd_dman_dt(jsonData[0]);
|
||||||
dataVO.setSnd_dman_id("");
|
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
|
||||||
dataVO.setSnd_dman_sno("1");
|
dataVO.setSnd_dman_sno("1");
|
||||||
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
|
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
|
||||||
dataVO.setCstno(""); // 고객번호(필수)
|
dataVO.setCstno(propertyService.getPropertyValue("Monitoring", "djb.ums.email.cstno", "")); // 고객번호(필수)
|
||||||
dataVO.setSendr_emaddr(""); // 발신자 이메일
|
dataVO.setSendr_emaddr(propertyService.getPropertyValue("Monitoring", "djb.ums.email.sendr_emaddr", "")); // 발신자 이메일
|
||||||
dataVO.setSendr_nm(""); // 발신자 이름
|
dataVO.setSendr_nm(propertyService.getPropertyValue("Monitoring", "djb.ums.email.sendr_nm", "")); // 발신자 이름
|
||||||
dataVO.setSndbk_emaddr(""); // 리턴 이메일
|
dataVO.setSndbk_emaddr(propertyService.getPropertyValue("Monitoring", "djb.ums.email.sndbk_emaddr", "")); // 리턴 이메일
|
||||||
dataVO.setRecvr_emaddr(messageRequest.getEmail()); // 수신자 이메일(필수)
|
dataVO.setRecvr_emaddr(messageRequest.getEmail()); // 수신자 이메일(필수)
|
||||||
dataVO.setRecvr_nm(messageRequest.getUsername()); // 수신자명(필수)
|
dataVO.setRecvr_nm(messageRequest.getUsername()); // 수신자명(필수)
|
||||||
dataVO.setTmplt_mpng_info(messageRequest.getMessage()); // 탬플릿매핑정보
|
dataVO.setTmplt_mpng_info(messageRequest.getMessage()); // 탬플릿매핑정보
|
||||||
dataVO.setTmplt_mpng_rptt_info(""); // 탬플릿매핑반복정보
|
dataVO.setTmplt_mpng_rptt_info(""); // 탬플릿매핑반복정보
|
||||||
dataVO.setSnd_empno("APIM"); // 발신자ID(필수)
|
dataVO.setSnd_empno("APIM"); // 발신자ID(필수)
|
||||||
dataVO.setSnd_emp_dprm_cd("132"); // 발신자부서코드(필수)
|
dataVO.setSnd_emp_dprm_cd("132"); // 발신자부서코드(필수)
|
||||||
dataVO.setBzwk_cd(BZWK_DVCD); // 업무구분코드
|
dataVO.setBzwk_cd(BZWK_CD); // 업무코드
|
||||||
dataVO.setSub_bzwk_cd(SUB_BZWK_CD); // 업무세부코드
|
dataVO.setSub_bzwk_cd(SUB_BZWK_CD); // 서브업무코드
|
||||||
dataVO.setSys_dvcd(SYS_DVCD);
|
dataVO.setSys_dvcd(SYS_DVCD); // 시스템구분코드
|
||||||
dataVO.setTx_dt(jsonData[0]); //거래일자
|
dataVO.setTx_dt(jsonData[0]); // 거래일자
|
||||||
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
|
dataVO.setTx_tktm(jsonData[1].substring(0,6)); // 거래시간
|
||||||
|
|
||||||
List<EmailDataVO> dataList = new ArrayList<EmailDataVO>();
|
List<EmailDataVO> dataList = new ArrayList<EmailDataVO>();
|
||||||
dataList.add(dataVO);
|
dataList.add(dataVO);
|
||||||
@@ -278,8 +280,8 @@ public class UmsService {
|
|||||||
// 헤더 + 데이터
|
// 헤더 + 데이터
|
||||||
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
|
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
|
||||||
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
|
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
|
||||||
+ " \"ums_if_data_ncnt\":1, \r\n"
|
+ " \"ums_if_data_ccnt\":1, \r\n"
|
||||||
+ " \"ums_if_data\":" + data
|
+ " \"eMAILSndLnkdInputDto\":" + data
|
||||||
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
|
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,18 +346,20 @@ public class UmsService {
|
|||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
|
||||||
String yyyymmdd = sdf.format(new Date());
|
String yyyymmdd = sdf.format(new Date());
|
||||||
|
|
||||||
String uuid = SYS_DVCD + BZWK_DVCD + SUB_BZWK_CD + yyyymmdd + "O" ;
|
String uuid = "";
|
||||||
|
|
||||||
if ("SMS".equals(messageType)) {
|
if ("EMAIL".equals(messageType)) {
|
||||||
uuid += "S";
|
uuid = SYS_DVCD + BZWK_CD + SUB_BZWK_CD + yyyymmdd + "O" + "E" ;
|
||||||
} else if ("LMS".equals(messageType)) {
|
|
||||||
uuid += "L";
|
long seq = messageRequestService.countEmailRequestsOnDate(LocalDate.now()) + 1;
|
||||||
} else if ("KAKAO".equals(messageType)) {
|
uuid += String.format("%013d", seq);
|
||||||
uuid += "K";
|
|
||||||
}
|
} else if ("SMS".equals(messageType)) { //kakao 알림톡
|
||||||
|
uuid = SYS_DVCD + BZWK_DVCD + BZWK_DCLS_DVCD + yyyymmdd + "O" + "K";
|
||||||
|
|
||||||
long seq = messageRequestService.countSmsKakaoRequestsOnDate(LocalDate.now()) + 1;
|
long seq = messageRequestService.countSmsKakaoRequestsOnDate(LocalDate.now()) + 1;
|
||||||
uuid += String.format("%014d", seq);
|
uuid += String.format("%013d", seq);
|
||||||
|
}
|
||||||
|
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ public class SmsDataVO {
|
|||||||
private String ltrs_snd_tycd;
|
private String ltrs_snd_tycd;
|
||||||
|
|
||||||
/** LMS 제목 */
|
/** LMS 제목 */
|
||||||
private String msg_titl;
|
private String sms_msg_titl;
|
||||||
|
|
||||||
/** 메세지 */
|
/** 메세지 */
|
||||||
private String msg_ctnt;
|
private String sms_msg_ctnt;
|
||||||
|
|
||||||
/** 수신자번호 */
|
/** 수신자번호 */
|
||||||
private String recvr_no;
|
private String recvr_no;
|
||||||
|
|||||||
+17
-18
@@ -85,14 +85,14 @@ public class WebhookReceiveService {
|
|||||||
log.info("[Webhook] 이벤트 처리 시작 - eventType: {}", eventType);
|
log.info("[Webhook] 이벤트 처리 시작 - eventType: {}", eventType);
|
||||||
|
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case "ORDER_CREATED":
|
case "CONTROL_START":
|
||||||
handleOrderCreated(data);
|
handleControlStart(data);
|
||||||
break;
|
break;
|
||||||
case "ORDER_CANCELLED":
|
case "CONTROL_END":
|
||||||
handleOrderCancelled(data);
|
handleControlEnd(data);
|
||||||
break;
|
break;
|
||||||
case "PAYMENT_COMPLETED":
|
case "ERROR_START":
|
||||||
handlePaymentCompleted(data);
|
handleErrorStart(data);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
log.warn("[Webhook] 처리되지 않은 이벤트 - eventType: {}", eventType);
|
log.warn("[Webhook] 처리되지 않은 이벤트 - eventType: {}", eventType);
|
||||||
@@ -106,20 +106,19 @@ public class WebhookReceiveService {
|
|||||||
/* 이벤트별 핸들러 (비즈니스 로직 구현) */
|
/* 이벤트별 핸들러 (비즈니스 로직 구현) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
private void handleOrderCreated(JsonNode data) {
|
private void handleControlStart(JsonNode data) {
|
||||||
log.info("[Webhook] ORDER_CREATED 처리 - data: {}", data);
|
log.info("[Webhook] CONTROL_START 처리 - data: {}", data);
|
||||||
// TODO: 주문 생성 처리 로직
|
// TODO: 점검시작 처리 로직
|
||||||
|
}
|
||||||
|
private void handleControlEnd(JsonNode data) {
|
||||||
|
log.info("[Webhook] CONTROL_END 처리 - data: {}", data);
|
||||||
|
// TODO: 점검종료 처리 로직
|
||||||
|
}
|
||||||
|
private void handleErrorStart(JsonNode data) {
|
||||||
|
log.info("[Webhook] ERROR_START 처리 - data: {}", data);
|
||||||
|
// TODO: 장애시작 처리 로직
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleOrderCancelled(JsonNode data) {
|
|
||||||
log.info("[Webhook] ORDER_CANCELLED 처리 - data: {}", data);
|
|
||||||
// TODO: 주문 취소 처리 로직
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePaymentCompleted(JsonNode data) {
|
|
||||||
log.info("[Webhook] PAYMENT_COMPLETED 처리 - data: {}", data);
|
|
||||||
// TODO: 결제 완료 처리 로직
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* 공통 유틸 */
|
/* 공통 유틸 */
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import com.eactive.eai.rms.data.entity.man.user.UserRoleId;
|
|||||||
import com.eactive.eai.rms.data.entity.man.user.UserRoleService;
|
import com.eactive.eai.rms.data.entity.man.user.UserRoleService;
|
||||||
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
|
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
|
||||||
import com.eactive.eai.rms.onl.apim.masking.MaskingUtils;
|
import com.eactive.eai.rms.onl.apim.masking.MaskingUtils;
|
||||||
import com.eactive.ext.kjb.common.KjbProperty;
|
//import com.eactive.ext.kjb.common.KjbProperty;
|
||||||
import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
//import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
||||||
import com.eactive.ext.kjb.sso.KjbSsoModule;
|
//import com.eactive.ext.kjb.sso.KjbSsoModule;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@@ -52,108 +52,108 @@ public class SsoController implements InterceptorSkipController {
|
|||||||
this.mainController = mainController;
|
this.mainController = mainController;
|
||||||
}
|
}
|
||||||
|
|
||||||
private KjbSsoModule getSsoModule() {
|
// private KjbSsoModule getSsoModule() {
|
||||||
return KjbSsoModule.getInstance();
|
// return KjbSsoModule.getInstance();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
// private KjbProperty getProperty() {
|
||||||
|
// return KjbPropertyHolder.get();
|
||||||
|
// }
|
||||||
|
|
||||||
private KjbProperty getProperty() {
|
// @RequestMapping(value = "/sso/login.do")
|
||||||
return KjbPropertyHolder.get();
|
// public String login(HttpServletRequest req, HttpServletResponse res,
|
||||||
}
|
// @RequestParam(name = "UURL", required = false) String uurl) throws Exception {
|
||||||
|
// HttpSession session = req.getSession();
|
||||||
@RequestMapping(value = "/sso/login.do")
|
// String ssoId = getSsoModule().getSsoId(req);
|
||||||
public String login(HttpServletRequest req, HttpServletResponse res,
|
//
|
||||||
@RequestParam(name = "UURL", required = false) String uurl) throws Exception {
|
// log.debug("ssoId : {}", ssoId);
|
||||||
HttpSession session = req.getSession();
|
// log.debug("uurl : {}", uurl);
|
||||||
String ssoId = getSsoModule().getSsoId(req);
|
//
|
||||||
|
// // 디버깅용 쿠키 목록 출력
|
||||||
log.debug("ssoId : {}", ssoId);
|
// if (log.isDebugEnabled()) {
|
||||||
log.debug("uurl : {}", uurl);
|
// Cookie[] cookies = req.getCookies();
|
||||||
|
// if (cookies == null || cookies.length == 0) {
|
||||||
// 디버깅용 쿠키 목록 출력
|
// log.debug("No cookies");
|
||||||
if (log.isDebugEnabled()) {
|
// } else {
|
||||||
Cookie[] cookies = req.getCookies();
|
// for (Cookie c : cookies) {
|
||||||
if (cookies == null || cookies.length == 0) {
|
// log.debug("Cookie[{}] : Domain={}, Path={}",
|
||||||
log.debug("No cookies");
|
// c.getName(), c.getDomain(), c.getPath());
|
||||||
} else {
|
// }
|
||||||
for (Cookie c : cookies) {
|
// }
|
||||||
log.debug("Cookie[{}] : Domain={}, Path={}",
|
// }
|
||||||
c.getName(), c.getDomain(), c.getPath());
|
//
|
||||||
}
|
// if (StringUtils.isEmpty(uurl)) {
|
||||||
}
|
// uurl = getSsoModule().getAscpUrl();
|
||||||
}
|
// log.debug("UURL is empty, set to ASCP URL");
|
||||||
|
// log.debug("uurl : {}", uurl);
|
||||||
if (StringUtils.isEmpty(uurl)) {
|
// }
|
||||||
uurl = getSsoModule().getAscpUrl();
|
//
|
||||||
log.debug("UURL is empty, set to ASCP URL");
|
// if (ssoId == null) {
|
||||||
log.debug("uurl : {}", uurl);
|
// getSsoModule().goLoginPage(res, uurl);
|
||||||
}
|
// return null;
|
||||||
|
// } else {
|
||||||
if (ssoId == null) {
|
// // EAM 세션 검증
|
||||||
getSsoModule().goLoginPage(res, uurl);
|
// String retCode = getSsoModule().getEamSessionCheck(req, res);
|
||||||
return null;
|
// log.debug("retCode : {}", retCode);
|
||||||
} else {
|
//
|
||||||
// EAM 세션 검증
|
// log.debug("Property - sso.ignore_validation = {}", getProperty().isSsoIgnoreValidation());
|
||||||
String retCode = getSsoModule().getEamSessionCheck(req, res);
|
// if (!getProperty().isSsoIgnoreValidation() && !"0".equalsIgnoreCase(retCode)) {
|
||||||
log.debug("retCode : {}", retCode);
|
// // SSO 검증 실패 시 에러 메시지 설정 및 로그인 페이지로 이동
|
||||||
|
// log.warn("SSO 검증 실패 - retCode: {}, ssoId: {}", retCode, ssoId);
|
||||||
log.debug("Property - sso.ignore_validation = {}", getProperty().isSsoIgnoreValidation());
|
// session.setAttribute("resultMsg", "SSO 로그인을 실패 하였습니다. (오류코드: " + retCode + ") 다른 로그인 방식을 시도 하세요");
|
||||||
if (!getProperty().isSsoIgnoreValidation() && !"0".equalsIgnoreCase(retCode)) {
|
// return "redirect:/loginForm.do";
|
||||||
// SSO 검증 실패 시 에러 메시지 설정 및 로그인 페이지로 이동
|
// }
|
||||||
log.warn("SSO 검증 실패 - retCode: {}, ssoId: {}", retCode, ssoId);
|
//
|
||||||
session.setAttribute("resultMsg", "SSO 로그인을 실패 하였습니다. (오류코드: " + retCode + ") 다른 로그인 방식을 시도 하세요");
|
// if (StringUtils.isEmpty(ssoId)) {
|
||||||
return "redirect:/loginForm.do";
|
// session.setAttribute("resultMsg", "SSO 인증 정보를 획득하지 못했습니다.");
|
||||||
}
|
// return "redirect:/loginForm.do";
|
||||||
|
// }
|
||||||
if (StringUtils.isEmpty(ssoId)) {
|
//
|
||||||
session.setAttribute("resultMsg", "SSO 인증 정보를 획득하지 못했습니다.");
|
// if (getProperty().isSsoSyncInfo()) {
|
||||||
return "redirect:/loginForm.do";
|
// // 기본 정보
|
||||||
}
|
// Properties info = getSsoModule().getUserInfos(ssoId);
|
||||||
|
// // 확장 정보
|
||||||
if (getProperty().isSsoSyncInfo()) {
|
// Properties extendedInfo = getSsoModule().getUserExFields(ssoId);
|
||||||
// 기본 정보
|
//
|
||||||
Properties info = getSsoModule().getUserInfos(ssoId);
|
// // 필수 필드 검증
|
||||||
// 확장 정보
|
// try {
|
||||||
Properties extendedInfo = getSsoModule().getUserExFields(ssoId);
|
// validateRequiredFields(info, extendedInfo);
|
||||||
|
// } catch (IllegalStateException e) {
|
||||||
// 필수 필드 검증
|
// log.warn("SSO 필수 필드 검증 실패: {}", e.getMessage());
|
||||||
try {
|
// session.setAttribute("resultMsg", e.getMessage());
|
||||||
validateRequiredFields(info, extendedInfo);
|
// return "redirect:/loginForm.do";
|
||||||
} catch (IllegalStateException e) {
|
// }
|
||||||
log.warn("SSO 필수 필드 검증 실패: {}", e.getMessage());
|
//
|
||||||
session.setAttribute("resultMsg", e.getMessage());
|
// // 사용자 정보 로깅 (개인정보 마스킹 처리)
|
||||||
return "redirect:/loginForm.do";
|
// String userId = info.getProperty("USERID");
|
||||||
}
|
// String name = info.getProperty("NAME");
|
||||||
|
// String email = info.getProperty("EMAIL");
|
||||||
// 사용자 정보 로깅 (개인정보 마스킹 처리)
|
// String cellPhoneNo = extendedInfo != null ? extendedInfo.getProperty("CELLPHONENO") : null;
|
||||||
String userId = info.getProperty("USERID");
|
//
|
||||||
String name = info.getProperty("NAME");
|
// log.info("SSO 로그인 시도 - userId: {}", userId);
|
||||||
String email = info.getProperty("EMAIL");
|
// log.debug("SSO 사용자 정보 - userId: {}, name: {}, phone: {}, email: {}",
|
||||||
String cellPhoneNo = extendedInfo != null ? extendedInfo.getProperty("CELLPHONENO") : null;
|
// userId,
|
||||||
|
// MaskingUtils.maskName(name),
|
||||||
log.info("SSO 로그인 시도 - userId: {}", userId);
|
// MaskingUtils.maskPhoneNumber(cellPhoneNo),
|
||||||
log.debug("SSO 사용자 정보 - userId: {}, name: {}, phone: {}, email: {}",
|
// MaskingUtils.maskEmailId(email));
|
||||||
userId,
|
//
|
||||||
MaskingUtils.maskName(name),
|
// // 사용자 생성 또는 업데이트
|
||||||
MaskingUtils.maskPhoneNumber(cellPhoneNo),
|
// UserInfo userInfo = findOrCreateUser(info, extendedInfo);
|
||||||
MaskingUtils.maskEmailId(email));
|
//
|
||||||
|
// // 로그인 세션 설정 (MainController 위임)
|
||||||
// 사용자 생성 또는 업데이트
|
// log.info("SSO 로그인 성공 - userId: {}", userId);
|
||||||
UserInfo userInfo = findOrCreateUser(info, extendedInfo);
|
// return mainController.setupSsoLoginSession(req, userInfo);
|
||||||
|
// } else {
|
||||||
// 로그인 세션 설정 (MainController 위임)
|
// // 동기화 비활성화 모드: 기존 사용자는 업데이트 없이, 신규 사용자는 최소 정보로 생성
|
||||||
log.info("SSO 로그인 성공 - userId: {}", userId);
|
// log.info("SSO 동기화 비활성화 모드로 로그인 처리 - ssoId: {}", ssoId);
|
||||||
return mainController.setupSsoLoginSession(req, userInfo);
|
//
|
||||||
} else {
|
// UserInfo userInfo = findOrCreateUserWithoutSync(ssoId);
|
||||||
// 동기화 비활성화 모드: 기존 사용자는 업데이트 없이, 신규 사용자는 최소 정보로 생성
|
//
|
||||||
log.info("SSO 동기화 비활성화 모드로 로그인 처리 - ssoId: {}", ssoId);
|
// log.info("SSO 로그인 성공 (동기화 비활성화 모드) - userId: {}", ssoId);
|
||||||
|
// return mainController.setupSsoLoginSession(req, userInfo);
|
||||||
UserInfo userInfo = findOrCreateUserWithoutSync(ssoId);
|
// }
|
||||||
|
// }
|
||||||
log.info("SSO 로그인 성공 (동기화 비활성화 모드) - userId: {}", ssoId);
|
// }
|
||||||
return mainController.setupSsoLoginSession(req, userInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 필수 필드 검증
|
* 필수 필드 검증
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.eactive.eai.rms.onl.apim.apigroup.mapping.ApiGroupUIMapper;
|
|||||||
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUI;
|
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUI;
|
||||||
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUISearch;
|
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUISearch;
|
||||||
import com.eactive.eai.transformer.util.Logger;
|
import com.eactive.eai.transformer.util.Logger;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -75,6 +76,7 @@ public class ApiGroupManService extends OnlBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void insert(ApiGroupUI apiGroupUI) {
|
public void insert(ApiGroupUI apiGroupUI) {
|
||||||
|
applyContentSubjectDefault(apiGroupUI);
|
||||||
ApiGroup apiGroup = apiGroupUIMapper.toEntity(apiGroupUI);
|
ApiGroup apiGroup = apiGroupUIMapper.toEntity(apiGroupUI);
|
||||||
for (ApiGroupApi apiGroupApi : apiGroup.getApiGroupApiList()){
|
for (ApiGroupApi apiGroupApi : apiGroup.getApiGroupApiList()){
|
||||||
apiGroupApi.setApiGroup(apiGroup);
|
apiGroupApi.setApiGroup(apiGroup);
|
||||||
@@ -86,11 +88,19 @@ public class ApiGroupManService extends OnlBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void update(ApiGroupUI apiGroupUI) {
|
public void update(ApiGroupUI apiGroupUI) {
|
||||||
|
applyContentSubjectDefault(apiGroupUI);
|
||||||
ApiGroup apiGroup = apiGroupService.getById(apiGroupUI.getId());
|
ApiGroup apiGroup = apiGroupService.getById(apiGroupUI.getId());
|
||||||
apiGroupUIMapper.updateToEntity(apiGroupUI, apiGroup);
|
apiGroupUIMapper.updateToEntity(apiGroupUI, apiGroup);
|
||||||
apiGroupService.save(apiGroup);
|
apiGroupService.save(apiGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 메인 그룹명(contentSubject) 미입력 시 API 그룹 명(groupName)으로 대체
|
||||||
|
private void applyContentSubjectDefault(ApiGroupUI apiGroupUI) {
|
||||||
|
if (StringUtils.isBlank(apiGroupUI.getContentSubject())) {
|
||||||
|
apiGroupUI.setContentSubject(apiGroupUI.getGroupName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void delete(ApiGroupUI apiGroupUI) {
|
public void delete(ApiGroupUI apiGroupUI) {
|
||||||
ApiGroup apiGroup = apiGroupService.getById(apiGroupUI.getId());
|
ApiGroup apiGroup = apiGroupService.getById(apiGroupUI.getId());
|
||||||
apiGroupService.delete(apiGroup);
|
apiGroupService.delete(apiGroup);
|
||||||
|
|||||||
+17
-2
@@ -66,8 +66,8 @@ public class PortalApprovalManController extends BaseAnnotationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=APPROVE")
|
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=APPROVE")
|
||||||
public ResponseEntity<Void> approve(String id) {
|
public ResponseEntity<Void> approve(String id, @RequestParam(required = false) String gwAction) {
|
||||||
portalApprovalManService.approve(id, SessionManager.getLoginVo().getUserId());
|
portalApprovalManService.approve(id, SessionManager.getLoginVo().getUserId(), gwAction);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +77,21 @@ public class PortalApprovalManController extends BaseAnnotationController {
|
|||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=FORCE_REJECT")
|
||||||
|
public ResponseEntity<Void> forceReject(String id, String rejectReason) {
|
||||||
|
if (StringUtils.isBlank(rejectReason)) {
|
||||||
|
throw new BizException("강제 반려 사유를 입력해 주세요.");
|
||||||
|
}
|
||||||
|
portalApprovalManService.forceReject(id, SessionManager.getLoginVo().getUserId(), rejectReason);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=DELETE")
|
||||||
|
public ResponseEntity<Void> delete(String id) {
|
||||||
|
portalApprovalManService.deleteApprovalRequest(id);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=REDEPLOY")
|
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=REDEPLOY")
|
||||||
public ResponseEntity<Void> redeploy(String id) {
|
public ResponseEntity<Void> redeploy(String id) {
|
||||||
portalApprovalManService.redeploy(id);
|
portalApprovalManService.redeploy(id);
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package com.eactive.eai.rms.onl.apim.approval;
|
|||||||
|
|
||||||
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
import com.eactive.apim.portal.apispec.service.ApiSpecInfoService;
|
||||||
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
||||||
|
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||||
|
import com.eactive.apim.portal.apprequest.entity.GwAction;
|
||||||
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||||
import com.eactive.apim.portal.approval.entity.Approval;
|
import com.eactive.apim.portal.approval.entity.Approval;
|
||||||
import com.eactive.apim.portal.approval.entity.ApprovalType;
|
import com.eactive.apim.portal.approval.entity.ApprovalType;
|
||||||
|
import com.eactive.apim.portal.approval.entity.ApproverStatus;
|
||||||
import com.eactive.apim.portal.approval.statemachine.*;
|
import com.eactive.apim.portal.approval.statemachine.*;
|
||||||
|
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||||
|
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.OrgStatus;
|
||||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||||
@@ -41,6 +46,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.PersistenceContext;
|
import javax.persistence.PersistenceContext;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -52,6 +58,7 @@ public class PortalApprovalManService extends BaseService {
|
|||||||
private static final String HARD_DELETE_FLAG_KEY = "approval.hard-delete.enabled";
|
private static final String HARD_DELETE_FLAG_KEY = "approval.hard-delete.enabled";
|
||||||
|
|
||||||
private final ApprovalService approvalService;
|
private final ApprovalService approvalService;
|
||||||
|
private final PortalApprovalUIMapper portalApprovalUIMapper;
|
||||||
private final ApprovalStateMachine approvalStateMachine;
|
private final ApprovalStateMachine approvalStateMachine;
|
||||||
private final AppRequestRepository appRequestRepository;
|
private final AppRequestRepository appRequestRepository;
|
||||||
private final PortalAppApprovalListener portalAppApprovalListener;
|
private final PortalAppApprovalListener portalAppApprovalListener;
|
||||||
@@ -73,12 +80,18 @@ public class PortalApprovalManService extends BaseService {
|
|||||||
private EntityManager entityManager;
|
private EntityManager entityManager;
|
||||||
|
|
||||||
public Page<PortalApprovalUI> selectList(Pageable pageable, PortalApprovalUISearch portalApprovalUISearch) {
|
public Page<PortalApprovalUI> selectList(Pageable pageable, PortalApprovalUISearch portalApprovalUISearch) {
|
||||||
|
// 목록 그리드는 Approval 자체 필드(제목/유형/상태/요청자/생성일/예상완료일)만 사용하므로
|
||||||
|
// 행별 상세 재조회(getDetailWithMaskOption: AppRequest + API 목록 + AGW 크로스 조회) 없이 경량 매핑한다.
|
||||||
Page<Approval> approval = approvalService.findAll(pageable, portalApprovalUISearch);
|
Page<Approval> approval = approvalService.findAll(pageable, portalApprovalUISearch);
|
||||||
return approval.map(entity -> {
|
return approval.map(entity -> {
|
||||||
try {
|
try {
|
||||||
return getDetailWithMaskOption(entity.getId(), false);
|
PortalApprovalUI ui = portalApprovalUIMapper.toVo(entity);
|
||||||
|
if (ui.getRequester() == null) {
|
||||||
|
ui.setRequester(createDeletedUserPlaceholder());
|
||||||
|
}
|
||||||
|
return ui;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("승인 상세 조회 실패 - ID: {}, 오류: {}", entity.getId(), e.getMessage());
|
logger.error("승인 목록 매핑 실패 - ID: {}, 오류: {}", entity.getId(), e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -200,6 +213,7 @@ public class PortalApprovalManService extends BaseService {
|
|||||||
if (optRequest.isPresent()) {
|
if (optRequest.isPresent()) {
|
||||||
AppRequest appRequest = optRequest.get();
|
AppRequest appRequest = optRequest.get();
|
||||||
appApprovalUI.setRequestType(appRequest.getType());
|
appApprovalUI.setRequestType(appRequest.getType());
|
||||||
|
appApprovalUI.setGwAction(appRequest.getGwAction());
|
||||||
|
|
||||||
|
|
||||||
if (appRequest.getApiGroupList() == null || appRequest.getApiGroupList().isEmpty()) {
|
if (appRequest.getApiGroupList() == null || appRequest.getApiGroupList().isEmpty()) {
|
||||||
@@ -317,7 +331,7 @@ public class PortalApprovalManService extends BaseService {
|
|||||||
return placeholder;
|
return placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void approve(String id, String approverId) {
|
public void approve(String id, String approverId, String gwAction) {
|
||||||
Map options = new HashMap<>();
|
Map options = new HashMap<>();
|
||||||
|
|
||||||
Approval approval = approvalService.findById(id).orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
|
Approval approval = approvalService.findById(id).orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
|
||||||
@@ -325,6 +339,18 @@ public class PortalApprovalManService extends BaseService {
|
|||||||
throw new IllegalArgumentException("아직 승인 차례가 아닙니다.");
|
throw new IllegalArgumentException("아직 승인 차례가 아닙니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API 이용해지(DELETE) 승인이면 GW 처리방식(차단/완전삭제)을 신청서에 영속화한다.
|
||||||
|
// 같은 EMS 트랜잭션이므로 최종 승인 시 listener 가 이 값을 읽고, REDEPLOY 재처리에서도 유지된다.
|
||||||
|
// 미지정(null)은 안전 기본값인 차단(BLOCK)으로 처리.
|
||||||
|
if (approval.getApprovalType().equals(ApprovalType.APP)) {
|
||||||
|
appRequestRepository.findById(approval.getTargetId()).ifPresent(request -> {
|
||||||
|
if (AppRequestType.DELETE.equals(request.getType())) {
|
||||||
|
request.setGwAction("DELETE".equalsIgnoreCase(gwAction) ? GwAction.DELETE : GwAction.BLOCK);
|
||||||
|
appRequestRepository.save(request);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
options.put(ApprovalConstants.APPROVER, approverId);
|
options.put(ApprovalConstants.APPROVER, approverId);
|
||||||
sendEvent(id, ApprovalEvent.APPROVE, options);
|
sendEvent(id, ApprovalEvent.APPROVE, options);
|
||||||
}
|
}
|
||||||
@@ -342,6 +368,51 @@ public class PortalApprovalManService extends BaseService {
|
|||||||
sendEvent(id, ApprovalEvent.DENY, options);
|
sendEvent(id, ApprovalEvent.DENY, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 강제 반려: 승인자 목록에 삭제된 사용자가 있어 승인 진행이 불가능한 건을
|
||||||
|
* 승인자가 아닌 관리자도 반려 처리할 수 있다. (요청됨/진행중 상태에서만 허용)
|
||||||
|
*/
|
||||||
|
public void forceReject(String id, String adminUserId, String rejectReason) {
|
||||||
|
Approval approval = approvalService.findById(id)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
|
||||||
|
|
||||||
|
ApprovalState currentStatus = approval.getApprovalStatus();
|
||||||
|
if (!(currentStatus instanceof RequestedState) && !(currentStatus instanceof ProcessingState)) {
|
||||||
|
throw new BizException("요청됨/진행중 상태의 승인 요청만 강제 반려할 수 있습니다.");
|
||||||
|
}
|
||||||
|
boolean hasDeletedApprover = approval.getApprovers().stream()
|
||||||
|
.anyMatch(approver -> approver.getUser() == null);
|
||||||
|
if (!hasDeletedApprover) {
|
||||||
|
throw new BizException("승인자 목록에 삭제된 사용자가 있는 경우에만 강제 반려할 수 있습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
approval.getApprovers().forEach(approver -> {
|
||||||
|
if (approver.getUser() == null
|
||||||
|
&& (ApproverStatus.CURRENT.equals(approver.getApproverStatus())
|
||||||
|
|| ApproverStatus.PENDING.equals(approver.getApproverStatus()))) {
|
||||||
|
approver.setApproverStatus(ApproverStatus.DENIED);
|
||||||
|
approver.setApprovedDate(LocalDateTime.now());
|
||||||
|
approver.setApprovalMessage("[관리자 강제 반려: " + adminUserId + "] " + rejectReason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
approval.setApprovalStatus(new DeniedState());
|
||||||
|
|
||||||
|
Map<String, Object> options = new HashMap<>();
|
||||||
|
options.put(ApprovalConstants.APPROVER, adminUserId);
|
||||||
|
options.put((String) ApprovalConstants.MESSAGE, rejectReason);
|
||||||
|
options.put("authorizer", portalApprovalAuthorizer);
|
||||||
|
if (approval.getApprovalType().equals(ApprovalType.APP)) {
|
||||||
|
options.put("listener", portalAppApprovalListener);
|
||||||
|
} else if (approval.getApprovalType().equals(ApprovalType.USER)) {
|
||||||
|
options.put("listener", portalUserApprovalListener);
|
||||||
|
}
|
||||||
|
// DENY 이벤트는 authorizer 검증에 걸리므로 상태를 직접 DENIED 로 만든 뒤 END 로 롤백만 수행
|
||||||
|
approvalStateMachine.transition(approval, ApprovalEvent.END, options);
|
||||||
|
|
||||||
|
approvalService.save(approval);
|
||||||
|
logger.info("Approval {} force-rejected by admin {} (deleted approver present)", id, adminUserId);
|
||||||
|
}
|
||||||
|
|
||||||
public void update(String id, String expectEndDate) {
|
public void update(String id, String expectEndDate) {
|
||||||
Approval approval = approvalService.findById(id)
|
Approval approval = approvalService.findById(id)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
|
.orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
|
||||||
@@ -385,6 +456,45 @@ public class PortalApprovalManService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 삭제된 대상 사용자 또는 삭제된 법인이 포함되어 승인·반려가 불가능한 승인 요청을 물리 삭제한다.
|
||||||
|
* 사용자(PortalUser)/법인(PortalOrg) row 는 건드리지 않고 PTL_APPROVER/PTL_APPROVAL row 만 제거한다.
|
||||||
|
* 임의 삭제 방지를 위해 실제로 삭제된 사용자/법인이 포함된 USER 유형 요청만 허용한다.
|
||||||
|
*/
|
||||||
|
public void deleteApprovalRequest(String id) {
|
||||||
|
Approval approval = approvalService.findById(id)
|
||||||
|
.orElseThrow(() -> new BizException("승인 정보를 찾을 수 없습니다. ID: " + id));
|
||||||
|
if (!approval.getApprovalType().equals(ApprovalType.USER)) {
|
||||||
|
throw new BizException("법인사용자 승인 요청만 삭제할 수 있습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<PortalUser> optUser = portalUserRepository.findById(approval.getTargetId());
|
||||||
|
boolean userDeleted = !optUser.isPresent()
|
||||||
|
|| "반려된 사용자".equals(optUser.get().getUserName())
|
||||||
|
|| "삭제된 사용자".equals(optUser.get().getUserName());
|
||||||
|
boolean orgDeleted = false;
|
||||||
|
if (optUser.isPresent()) {
|
||||||
|
PortalOrg org = optUser.get().getPortalOrg();
|
||||||
|
orgDeleted = org == null
|
||||||
|
|| OrgStatus.REMOVED.equals(org.getOrgStatus())
|
||||||
|
|| "삭제된법인".equals(org.getOrgName());
|
||||||
|
}
|
||||||
|
if (!(userDeleted || orgDeleted)) {
|
||||||
|
throw new BizException("삭제된 사용자 또는 법인이 포함된 요청만 삭제할 수 있습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
entityManager.createNativeQuery("DELETE FROM PTL_APPROVER WHERE APPROVAL_ID = :id")
|
||||||
|
.setParameter("id", id)
|
||||||
|
.executeUpdate();
|
||||||
|
int affected = entityManager.createNativeQuery("DELETE FROM PTL_APPROVAL WHERE ID = :id")
|
||||||
|
.setParameter("id", id)
|
||||||
|
.executeUpdate();
|
||||||
|
if (affected == 0) {
|
||||||
|
throw new BizException("승인 정보를 찾을 수 없습니다. ID: " + id);
|
||||||
|
}
|
||||||
|
logger.info("Approval {} deleted (deleted target user/org present)", id);
|
||||||
|
}
|
||||||
|
|
||||||
public void sendEvent(String id, ApprovalEvent event, Map<String, Object> options) {
|
public void sendEvent(String id, ApprovalEvent event, Map<String, Object> options) {
|
||||||
approvalService.findById(id).ifPresent(approval -> {
|
approvalService.findById(id).ifPresent(approval -> {
|
||||||
options.put("authorizer", portalApprovalAuthorizer);
|
options.put("authorizer", portalApprovalAuthorizer);
|
||||||
|
|||||||
+35
-3
@@ -2,6 +2,7 @@ package com.eactive.eai.rms.onl.apim.approval.app;
|
|||||||
|
|
||||||
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
||||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||||
|
import com.eactive.apim.portal.apprequest.entity.GwAction;
|
||||||
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
||||||
import com.eactive.apim.portal.approval.entity.Approval;
|
import com.eactive.apim.portal.approval.entity.Approval;
|
||||||
import com.eactive.apim.portal.approval.service.ApprovalDeployException;
|
import com.eactive.apim.portal.approval.service.ApprovalDeployException;
|
||||||
@@ -72,7 +73,7 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
|||||||
|
|
||||||
// 위 메서드가 반환되면 트랜잭션이 커밋되어 Portal DB에 영구 저장됨
|
// 위 메서드가 반환되면 트랜잭션이 커밋되어 Portal DB에 영구 저장됨
|
||||||
// 이제 APIGW 동기화 및 GW 리로드 수행
|
// 이제 APIGW 동기화 및 GW 리로드 수행
|
||||||
syncTargetServer(type, credentialUI);
|
syncTargetServer(appRequest, credentialUI);
|
||||||
sendApprovalResult(appRequest);
|
sendApprovalResult(appRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +95,12 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
|||||||
} else if (type.equals(AppRequestType.MODIFY)) {
|
} else if (type.equals(AppRequestType.MODIFY)) {
|
||||||
credentialUI = modifyClientUI(appRequest);
|
credentialUI = modifyClientUI(appRequest);
|
||||||
} else if (type.equals(AppRequestType.DELETE)) {
|
} else if (type.equals(AppRequestType.DELETE)) {
|
||||||
|
// REDEPLOY 재처리 시 credential 이 이미 삭제되어 있을 수 있으므로 부재는 정상 통과시킨다.
|
||||||
|
try {
|
||||||
credentialManService.delete(appRequest.getClientId());
|
credentialManService.delete(appRequest.getClientId());
|
||||||
|
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
|
||||||
|
logger.warn("포털 credential 이미 삭제됨 (재처리 통과) - clientId: {}", appRequest.getClientId());
|
||||||
|
}
|
||||||
credentialUI = new CredentialUI();
|
credentialUI = new CredentialUI();
|
||||||
credentialUI.setClientid(appRequest.getClientId());
|
credentialUI.setClientid(appRequest.getClientId());
|
||||||
}
|
}
|
||||||
@@ -119,7 +125,9 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
|||||||
messageSendService.sendMessage(MessageCode.APP_APPROVE, recipient, params);
|
messageSendService.sendMessage(MessageCode.APP_APPROVE, recipient, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void syncTargetServer(AppRequestType action, CredentialUI credentialUI) throws ApprovalDeployException {
|
private void syncTargetServer(AppRequest appRequest, CredentialUI credentialUI) throws ApprovalDeployException {
|
||||||
|
|
||||||
|
AppRequestType action = appRequest.getType();
|
||||||
|
|
||||||
DataSourceType type = DataSourceTypeManager.getDataSourceType("APIGW");
|
DataSourceType type = DataSourceTypeManager.getDataSourceType("APIGW");
|
||||||
DataSourceContextHolder.setDataSourceType(type);
|
DataSourceContextHolder.setDataSourceType(type);
|
||||||
@@ -129,7 +137,7 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
|||||||
} else if (action.equals(AppRequestType.MODIFY)) {
|
} else if (action.equals(AppRequestType.MODIFY)) {
|
||||||
clientManService.update(credentialUIMapper.mapClient(credentialUI));
|
clientManService.update(credentialUIMapper.mapClient(credentialUI));
|
||||||
} else if (action.equals(AppRequestType.DELETE)) {
|
} else if (action.equals(AppRequestType.DELETE)) {
|
||||||
clientManService.delete(credentialUI.getClientid());
|
applyGwActionForDelete(appRequest.getGwAction(), credentialUI.getClientid());
|
||||||
}
|
}
|
||||||
|
|
||||||
CommonCommand.builder()
|
CommonCommand.builder()
|
||||||
@@ -138,6 +146,30 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
|||||||
.build().broadcast(agentUtilService);
|
.build().broadcast(agentUtilService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 이용해지 승인 시 GW 클라이언트(TSEAIAU01) 처리.
|
||||||
|
* 승인 시점에 관리자가 선택해 PTL_APP_REQUEST.GW_ACTION 에 영속화된 값을 따른다.
|
||||||
|
* - DELETE: 행 완전 삭제 / - BLOCK 또는 null(미지정): 차단(appstatus='0')
|
||||||
|
* GW 에 행이 이미 없으면(선삭제·재처리) 목적이 달성된 상태이므로 통과시키고,
|
||||||
|
* 그 외 오류는 ApprovalDeployException 으로 던져 FAILED(재처리 가능) 상태를 만든다.
|
||||||
|
*/
|
||||||
|
private void applyGwActionForDelete(GwAction gwAction, String clientId) throws ApprovalDeployException {
|
||||||
|
try {
|
||||||
|
if (GwAction.DELETE.equals(gwAction)) {
|
||||||
|
clientManService.delete(clientId);
|
||||||
|
logger.info("GW 클라이언트 완전 삭제 - clientId: {}", clientId);
|
||||||
|
} else {
|
||||||
|
clientManService.blockClient(clientId);
|
||||||
|
logger.info("GW 클라이언트 차단(appstatus=0) - clientId: {}", clientId);
|
||||||
|
}
|
||||||
|
} catch (org.springframework.dao.EmptyResultDataAccessException | javax.persistence.EntityNotFoundException e) {
|
||||||
|
logger.warn("GW 클라이언트가 이미 존재하지 않아 처리 생략 (재처리 통과) - clientId: {}", clientId);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
logger.error("GW 클라이언트 해지 처리 실패 - clientId: {}, gwAction: {}", clientId, gwAction, e);
|
||||||
|
throw new ApprovalDeployException("GW 클라이언트 해지 처리에 실패했습니다: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private CredentialUI modifyClientUI(AppRequest appRequest) {
|
private CredentialUI modifyClientUI(AppRequest appRequest) {
|
||||||
CredentialUI existingCredential = credentialManService.selectDetail(appRequest.getClientId());
|
CredentialUI existingCredential = credentialManService.selectDetail(appRequest.getClientId());
|
||||||
existingCredential.getApiList().clear();
|
existingCredential.getApiList().clear();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.eactive.eai.rms.onl.apim.approval.app;
|
package com.eactive.eai.rms.onl.apim.approval.app;
|
||||||
|
|
||||||
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
import com.eactive.apim.portal.apprequest.entity.AppRequestType;
|
||||||
|
import com.eactive.apim.portal.apprequest.entity.GwAction;
|
||||||
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUI;
|
import com.eactive.eai.rms.onl.apim.apigroup.ui.ApiGroupUI;
|
||||||
import com.eactive.eai.rms.onl.apim.approval.PortalApprovalUI;
|
import com.eactive.eai.rms.onl.apim.approval.PortalApprovalUI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -26,4 +27,7 @@ public class PortalAppApprovalUI extends PortalApprovalUI {
|
|||||||
private List<String> prevApiGroupNameList = new ArrayList<>();
|
private List<String> prevApiGroupNameList = new ArrayList<>();
|
||||||
|
|
||||||
private AppRequestType requestType;
|
private AppRequestType requestType;
|
||||||
|
|
||||||
|
//DELETE 승인 시 선택된 GW 처리방식 (승인 완료 건 상세 표시용)
|
||||||
|
private GwAction gwAction;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -82,9 +82,9 @@ public class MessageRequestManService extends BaseService {
|
|||||||
return t == null ? "" : t.format(f);
|
return t == null ? "" : t.format(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 상태 검색 콤보 (PENDING/SENT/FAILED). */
|
/** 상태 검색 콤보 (PENDING/SENT/FAILED/SKIPPED). SKIPPED 는 행번 아닌 ID 로 발송을 건너뛴 건. */
|
||||||
public List<ComboVo> statusCombo() {
|
public List<ComboVo> statusCombo() {
|
||||||
return Arrays.stream(new String[]{"PENDING", "SENT", "FAILED"})
|
return Arrays.stream(new String[]{"PENDING", "SENT", "FAILED", "SKIPPED"})
|
||||||
.map(s -> {
|
.map(s -> {
|
||||||
ComboVo v = new ComboVo();
|
ComboVo v = new ComboVo();
|
||||||
v.setCode(s);
|
v.setCode(s);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user