Compare commits
12 Commits
608cb4188e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| af8a9cca51 | |||
| 39a09d0f5d | |||
| 5b940717c4 | |||
| 8ccf52fd66 | |||
| dedc58a07f | |||
| 4530af9dec | |||
| cbf3066a56 | |||
| 160e28444a | |||
| d7dcf9a81c | |||
| acfe6b2d03 | |||
| 481e82fec7 | |||
| 1b382f5859 |
@@ -16,3 +16,4 @@ PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK,DELETE
|
||||
PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE
|
||||
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
|
||||
MessageRequestManController_메세지발송내역_APIGW_UPDATE_STATUS
|
||||
PortalInquiryManController_Q&A문의관리_APIGW_VISIBILITY
|
||||
|
||||
@@ -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"?>
|
||||
<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">
|
||||
<weblogic-web-app xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app">
|
||||
|
||||
<context-root>monitoring</context-root>
|
||||
<session-descriptor>
|
||||
<timeout-secs>1800</timeout-secs>
|
||||
<cookie-name>JSESSIONID_PORTAL</cookie-name>
|
||||
<persistent-store-type>replicated_if_clustered</persistent-store-type>
|
||||
</session-descriptor>
|
||||
|
||||
|
||||
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; }
|
||||
@@ -298,6 +298,7 @@ $(document).ready(function() {
|
||||
url: url,
|
||||
data: postData,
|
||||
success: function(args) {
|
||||
console.log('mod', args);
|
||||
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
|
||||
type: 'success',
|
||||
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-sub" id="summaryThresholdCalc"></span>
|
||||
</div>
|
||||
<div class="bucket-summary-item">
|
||||
<span class="bucket-summary-label">적용 API</span>
|
||||
<span class="bucket-summary-value" id="summaryActiveApi">0<span class="unit">개</span></span>
|
||||
<span class="bucket-summary-sub" id="summaryActiveApiList"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<script language="javascript" >
|
||||
var url = '<c:url value="/onl/apim/apigroup/apiGroupMan.json"/>';
|
||||
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;
|
||||
let dialog;
|
||||
|
||||
@@ -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) {
|
||||
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;">' : '';
|
||||
@@ -101,13 +112,14 @@
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData : { cmd : 'LIST' },
|
||||
colNames:['No.', 'id', '제목', '법인명', '상태', '작성자', '등록일', '답변자','답변일'],
|
||||
colNames:['No.', 'id', '제목', '법인명', '상태', '공개범위', '작성자', '등록일', '답변자','답변일'],
|
||||
colModel:[
|
||||
{ name : 'rowNum' , align:'center', width:50, sortable:false },
|
||||
{ name : 'id' , align:'center', key:true, hidden:true},
|
||||
{ name : 'inquirySubject' , align:'left' , width:200 , formatter: formatFile},
|
||||
{ name : 'orgName' , align:'center' , width:100 , },
|
||||
{ name : 'inquiryStatus' , align:'center', width:70, formatter: formatInquiryStatus },
|
||||
{ name : 'visibility' , align:'center', width:70, formatter: formatVisibility },
|
||||
{ name : 'inquirer.userName', align:'center', width:70 },
|
||||
{ name : 'createdDate' , align:'center', width:100, formatter: timeStampFormat },
|
||||
{ name : 'responderName' , align:'center', width:70 },
|
||||
|
||||
@@ -79,12 +79,46 @@
|
||||
margin-left: 2px;
|
||||
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>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />';
|
||||
var file_download = '<c:url value="/file/download.do" />';
|
||||
var inquiryStatus = 'PENDING';
|
||||
var originalVisibility = 'ORG';
|
||||
var reasonModalCallback = null;
|
||||
|
||||
function decodeHTMLEntities(text) {
|
||||
var textArea = document.createElement('textarea');
|
||||
@@ -124,6 +158,24 @@
|
||||
var decodedContent = decodeHTMLEntities(data.responseDetail);
|
||||
$("#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);
|
||||
|
||||
@@ -149,8 +201,17 @@
|
||||
if (!data || data.length === 0) return;
|
||||
$.each(data, function (i, comment) {
|
||||
var date = comment.createdDate ? timeStampFormat(comment.createdDate) : '';
|
||||
var isPrivate = comment.visibility === 'PRIVATE';
|
||||
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');
|
||||
$('<div>').css('white-space', 'pre-wrap').text(comment.commentDetail).appendTo(content);
|
||||
var meta = $('<div>').css({'font-size': '0.9em', 'color': '#999', 'margin-top': '2px'});
|
||||
@@ -161,6 +222,19 @@
|
||||
})(comment.id, comment.inquiryId))
|
||||
.appendTo(meta);
|
||||
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);
|
||||
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) {
|
||||
if (inquiryStatus == 'CLOSED') {
|
||||
alert('답변이 종료되어 삭제할 수 없습니다');
|
||||
@@ -210,24 +362,38 @@
|
||||
alert('답변이 종료되어 수정할 수 없습니다');
|
||||
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 (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
|
||||
saveInquiryAnswer(returnUrl);
|
||||
});
|
||||
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
postData.push({name: "cmd", value: "INSERT"});
|
||||
$("#reasonModalOk").click(function () {
|
||||
var reason = ($('#reasonModalInput').val() || '').trim();
|
||||
if (!reason) {
|
||||
alert('변경 사유를 입력하세요.');
|
||||
return;
|
||||
}
|
||||
var cb = reasonModalCallback;
|
||||
closeReasonModal();
|
||||
if (cb) cb(reason);
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: postData,
|
||||
success: function (json) {
|
||||
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
$("#reasonModalCancel").click(function () {
|
||||
closeReasonModal();
|
||||
});
|
||||
|
||||
$("#btn_comment").click(function () {
|
||||
@@ -310,7 +476,22 @@
|
||||
<div class="info-label">작성일</div>
|
||||
<div id="createdDate" class="info-content"></div>
|
||||
</div>
|
||||
<div class="info-row" id="attachFileRow" style="display: none;">
|
||||
<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-label">첨부파일</div>
|
||||
<div id="attachFiles" class="info-content"></div>
|
||||
</div>
|
||||
@@ -358,5 +539,17 @@
|
||||
</form>
|
||||
</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>
|
||||
</html>
|
||||
@@ -86,9 +86,11 @@
|
||||
editurl: "clientArray",
|
||||
colNames: ['<%= localeMessage.getString("propertyDetail.propertyKey") %>',
|
||||
'<%= localeMessage.getString("propertyDetail.propertyValue") %>',
|
||||
'설명',
|
||||
'<%= localeMessage.getString("propertyDetail.delYn") %>'],
|
||||
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',
|
||||
width: 20,
|
||||
@@ -258,6 +260,7 @@
|
||||
var data = new Object();
|
||||
data["PRPTYNAME"] = $("input[name=prptyName]").val();
|
||||
data["PRPTY2VAL"] = $("input[name=prpty2Val]").val();
|
||||
data["PRPTYDESC"] = $("input[name=prptyDesc]").val();
|
||||
|
||||
var rows = $("#grid")[0].rows;
|
||||
var index = Number($(rows[rows.length - 1]).attr("id"));
|
||||
@@ -340,6 +343,11 @@
|
||||
<td colspan="3"><input type="text" name="prpty2Val"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설명</th>
|
||||
<td colspan="3"><input type="text" name="prptyDesc"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- grid -->
|
||||
<table id="grid"></table>
|
||||
|
||||
@@ -77,13 +77,17 @@
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
/* iframe 모달(showModal) 콘텐츠 패딩 제거 — iframe 이 다이얼로그 폭을 최대한 사용해 내부 가로스크롤 방지 */
|
||||
.ui-dialog .ui-dialog-content { padding: 0 !important; }
|
||||
|
||||
</style>
|
||||
<script language="javascript" >
|
||||
var isDetail = false;
|
||||
var url ='<c:url value="/onl/transaction/apim/apiInterfaceMan.json" />';
|
||||
const url_excel ='<c:url value="/onl/transaction/apim/apiInterfaceMan.excel" />';
|
||||
var url_spec = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
|
||||
var url_openapi_spec = '<c:url value="/onl/transaction/apim/djbApiSpecMan.view"/>';
|
||||
var returnUrl ;
|
||||
let headerValues = [];
|
||||
let headerNames = [];
|
||||
@@ -993,6 +997,33 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
makeValidate();
|
||||
// var bootstrapButton = $.fn.button.noConflict()
|
||||
@@ -1007,9 +1038,14 @@
|
||||
}
|
||||
init(url,key,detail);
|
||||
|
||||
// v1: 기존 API 스펙 (모달) — 기능 비교용 복원
|
||||
$("#btn_api_spec").click(function() {
|
||||
showApiSpecPopup();
|
||||
});
|
||||
// v2: 신규 OpenAPI Spec (새 탭)
|
||||
$("#btn_openapi_spec").click(function(e) {
|
||||
showOpenApiSpecPopup(e);
|
||||
});
|
||||
|
||||
$("#btn_modify").click(modifyApi);
|
||||
$("#btn_delete").click(function(){
|
||||
@@ -1508,7 +1544,10 @@
|
||||
<button type="button" class="cssbtn" id="btn_json_export" level="R" status="DETAIL,NEW"><i class="material-icons">download</i> <%=localeMessage.getString("button.exportJson")%></button></button>
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="R" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
|
||||
<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,NEW"><i class="material-icons">description</i> API 스펙(삭제예정)</button>
|
||||
<!-- v2: 신규 OpenAPI Spec (새 탭) -->
|
||||
<button type="button" class="cssbtn" id="btn_openapi_spec" level="R" status="DETAIL,NEW"><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>
|
||||
</div>
|
||||
<div class="title" id="title" style="font-size:1.4em">${rmsMenuName}</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</style>
|
||||
<script language="javascript">
|
||||
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 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,173 @@
|
||||
<%@ 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}",
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
+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
+6
-11
@@ -4,7 +4,6 @@ plugins {
|
||||
id 'eclipse-wtp'
|
||||
id 'idea'
|
||||
id 'war'
|
||||
id 'com.diffplug.eclipse.apt' version '3.41.1'
|
||||
}
|
||||
|
||||
group 'com.eactive'
|
||||
@@ -49,10 +48,6 @@ compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs = ['-parameters']
|
||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||
|
||||
aptOptions {
|
||||
processorArgs = [ 'querydsl.generatedAnnotationClass' : 'com.querydsl.core.annotations.Generated' ]
|
||||
}
|
||||
}
|
||||
|
||||
war {
|
||||
@@ -89,7 +84,7 @@ dependencies {
|
||||
implementation project(':elink-online-emsclient')
|
||||
implementation project(':elink-portal-common')
|
||||
//implementation project(':kjb-safedb')
|
||||
implementation project(":eapim-admin-djb")
|
||||
//implementation project(":eapim-admin-djb")
|
||||
|
||||
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
@@ -257,11 +252,11 @@ eclipse {
|
||||
}
|
||||
synchronizationTasks settingEclipseEncoding, initDirs
|
||||
jdt {
|
||||
apt {
|
||||
// generated 된 패스 경로를 .setting/org.eclipse.jdt.apt.core.prefs 의 org.eclipse.jdt.apt.genSrcDir에 적용한다.
|
||||
// project > properties > Java Compiler > Annoation Processing 화면에서 확인 가능하다.
|
||||
genSrcDir = file(generatedJavaDir)
|
||||
}
|
||||
// apt {
|
||||
// // generated 된 패스 경로를 .setting/org.eclipse.jdt.apt.core.prefs 의 org.eclipse.jdt.apt.genSrcDir에 적용한다.
|
||||
// // project > properties > Java Compiler > Annoation Processing 화면에서 확인 가능하다.
|
||||
// genSrcDir = file(generatedJavaDir)
|
||||
// }
|
||||
}
|
||||
project {
|
||||
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
|
||||
|
||||
@@ -87,8 +87,8 @@ dependencies {
|
||||
implementation project(":eapim-admin-djb")
|
||||
|
||||
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
compileOnly files('libs/damo-manager.jar')
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
compileOnly files('libs/damo-manager.jar')
|
||||
|
||||
//implementation "org.dom4j:dom4j:2.1.3"
|
||||
//implementation "com.rabbitmq:amqp-client:3.6.6"
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ include 'elink-online-common'
|
||||
include 'elink-online-emsclient'
|
||||
include 'elink-portal-common'
|
||||
//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').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-portal-common').projectDir = new File(settingsDir, "../elink-portal-common")
|
||||
//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")
|
||||
@@ -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.service.MonitoringPropertyGroupService;
|
||||
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
|
||||
import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
||||
import com.eactive.ext.kjb.util.KjbPropertyInjector;
|
||||
import com.eactive.eai.rms.ext.djb.common.DjbPropertyHolder;
|
||||
import com.eactive.eai.rms.ext.djb.common.DjbPropertyInjector;
|
||||
|
||||
@Service("monitoringContext")
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
@@ -45,7 +45,7 @@ class MonitoringContextImpl implements MonitoringContext {
|
||||
|
||||
public void refresh() {
|
||||
init();
|
||||
KjbPropertyHolder.reload();
|
||||
DjbPropertyHolder.reload();
|
||||
}
|
||||
|
||||
public void refresh(String propName) {
|
||||
@@ -85,9 +85,9 @@ class MonitoringContextImpl implements MonitoringContext {
|
||||
|
||||
setPortMapper(monitoringContextDAO.getPortMap());
|
||||
|
||||
// KjbPropertyHolder 초기화
|
||||
KjbPropertyInjector injector = new KjbPropertyInjector(monitoringPropertyService, "Monitoring");
|
||||
KjbPropertyHolder.initialize(injector::inject);
|
||||
// DjbPropertyHolder 초기화
|
||||
DjbPropertyInjector injector = new DjbPropertyInjector(monitoringPropertyService, "Monitoring");
|
||||
DjbPropertyHolder.initialize(injector::inject);
|
||||
|
||||
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 com.eactive.eai.common.seed.Seed;
|
||||
import com.eactive.eai.rms.ext.kjb.smsauth.SmsAuthService;
|
||||
import com.eactive.ext.djb.DamoManager;
|
||||
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
|
||||
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.UserRoleService;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
|
||||
import com.eactive.eai.rms.ext.djb.smsauth.SmsAuthService;
|
||||
|
||||
@Controller
|
||||
public class MainController implements InterceptorSkipController {
|
||||
|
||||
@@ -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.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.QApiGroupApi;
|
||||
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||
@@ -75,4 +76,37 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
|
||||
.where(qApiGroup.id.in(apiGroupIds))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -14,6 +14,8 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Service
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
public class MessageRequestService
|
||||
@@ -53,6 +55,16 @@ public class MessageRequestService
|
||||
return repository.findAll(predicate, fixed);
|
||||
}
|
||||
|
||||
/** 특정 일자에 SMS/KAKAO 로 발송 요청된 건수 (UMS 일련번호 채번용). */
|
||||
public long countSmsKakaoRequestsOnDate(LocalDate date) {
|
||||
QMessageRequest q = QMessageRequest.messageRequest;
|
||||
BooleanBuilder predicate = new BooleanBuilder();
|
||||
predicate.and(q.messageType.in("SMS", "KAKAO"));
|
||||
predicate.and(q.requestDate.goe(date.atStartOfDay()));
|
||||
predicate.and(q.requestDate.loe(date.atTime(23, 59, 59)));
|
||||
return repository.count(predicate);
|
||||
}
|
||||
|
||||
/** PENDING 상태인 건만 FAILED 로 전환한다. */
|
||||
public void updateStatusToFailedIfPending(String id) {
|
||||
MessageRequest entity = getById(id);
|
||||
|
||||
@@ -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.MonitoringPropertyId;
|
||||
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 org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -12,52 +10,49 @@ import java.lang.reflect.Field;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
public class KjbPropertyInjector {
|
||||
public class DjbPropertyInjector {
|
||||
private final MonitoringPropertyService monitoringPropertyService;
|
||||
private final String groupId;
|
||||
|
||||
|
||||
public KjbPropertyInjector(MonitoringPropertyService monitoringPropertyService, String groupId) {
|
||||
public DjbPropertyInjector(MonitoringPropertyService monitoringPropertyService, String groupId) {
|
||||
this.monitoringPropertyService = monitoringPropertyService;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* KjbProperty의 URL 값들이 설정되어 있는지 확인
|
||||
* @param property 검사할 KjbProperty 객체
|
||||
* DjbProperty의 URL 값들이 설정되어 있는지 확인
|
||||
* @param property 검사할 DjbProperty 객체
|
||||
* @return URL 값들이 모두 설정되어 있으면 true
|
||||
*/
|
||||
public boolean isUrlConfigured(KjbProperty property) {
|
||||
public boolean isUrlConfigured(DjbProperty property) {
|
||||
if (property == null) return false;
|
||||
|
||||
// 주요 URL 값들 중 하나라도 설정되어 있으면 true
|
||||
return StringUtils.isNotEmpty(property.getEaiBatchUrl())
|
||||
|| StringUtils.isNotEmpty(property.getUmsHostUrl())
|
||||
|| StringUtils.isNotEmpty(property.getObpUrl());
|
||||
return StringUtils.isNotEmpty(property.getApiAllowIpList());
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 값이 비어있을 경우 DB에서 재로딩 시도
|
||||
* @param property 기존 KjbProperty 객체
|
||||
* @return 재로딩된 KjbProperty 객체 (재로딩 실패 시 기존 객체 반환)
|
||||
* @param property 기존 DjbProperty 객체
|
||||
* @return 재로딩된 DjbProperty 객체 (재로딩 실패 시 기존 객체 반환)
|
||||
*/
|
||||
public KjbProperty reloadIfUrlEmpty(KjbProperty property) {
|
||||
public DjbProperty reloadIfUrlEmpty(DjbProperty property) {
|
||||
if (property == null) {
|
||||
return inject(new KjbProperty());
|
||||
return inject(new DjbProperty());
|
||||
}
|
||||
|
||||
if (!isUrlConfigured(property)) {
|
||||
log.info("KjbProperty URL 값이 비어있어 DB에서 재로딩 시도");
|
||||
log.info("DjbProperty URL 값이 비어있어 DB에서 재로딩 시도");
|
||||
try {
|
||||
KjbProperty reloaded = inject(new KjbProperty());
|
||||
DjbProperty reloaded = inject(new DjbProperty());
|
||||
if (isUrlConfigured(reloaded)) {
|
||||
log.info("KjbProperty 재로딩 성공");
|
||||
log.info("DjbProperty 재로딩 성공");
|
||||
return reloaded;
|
||||
} else {
|
||||
log.warn("KjbProperty 재로딩 후에도 URL 값이 비어있음");
|
||||
log.warn("DjbProperty 재로딩 후에도 URL 값이 비어있음");
|
||||
}
|
||||
} 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();
|
||||
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
KjbPropertyValue anno = field.getAnnotation(KjbPropertyValue.class);
|
||||
DjbPropertyValue anno = field.getAnnotation(DjbPropertyValue.class);
|
||||
|
||||
if (anno != null) {
|
||||
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
-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.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.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.time.LocalDateTime;
|
||||
@@ -13,11 +13,9 @@ import org.springframework.stereotype.Service;
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||
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.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.extern.slf4j.Slf4j;
|
||||
@@ -46,13 +44,11 @@ public class SmsAuthService {
|
||||
|
||||
private final UmsManager ums;
|
||||
|
||||
private static KjbProperty getProperty() {
|
||||
return KjbPropertyHolder.get();
|
||||
private static DjbProperty getProperty() {
|
||||
return DjbPropertyHolder.get();
|
||||
}
|
||||
|
||||
private KjbUmsService getUmsService() {
|
||||
return KjbUmsService.getInstance();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* SMS 인증이 필요한지 확인
|
||||
@@ -86,7 +82,7 @@ public class SmsAuthService {
|
||||
* 인증번호 생성
|
||||
*/
|
||||
public String generateAuthCode() {
|
||||
KjbProperty prop = getProperty();
|
||||
DjbProperty prop = getProperty();
|
||||
String mode = prop.getSmsAuthMode();
|
||||
|
||||
if (MODE_FIXED.equalsIgnoreCase(mode)) {
|
||||
@@ -113,7 +109,7 @@ public class SmsAuthService {
|
||||
HashMap<String,Object> params = new HashMap<String, Object>();
|
||||
params.put("authNumber", authCode);
|
||||
params.put("userName", userInfo.getUsername());
|
||||
log.debug("\n\n\nDDDDDDDDD" + userInfo.toString());
|
||||
|
||||
ums.send(userInfo, MessageCode.ADMIN_VERIFICATION_MOBILEPHONE, params);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
@@ -8,6 +8,7 @@ import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -18,8 +19,10 @@ import org.springframework.stereotype.Service;
|
||||
import com.eactive.apim.portal.common.util.PhoneNumberUtil;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.apim.portal.template.entity.MessageRequest;
|
||||
import com.eactive.eai.common.util.StringUtil;
|
||||
import com.eactive.eai.common.util.SystemUtil;
|
||||
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestService;
|
||||
import com.eactive.eai.rms.ext.djb.ums.vo.EmailDataVO;
|
||||
import com.eactive.eai.rms.ext.djb.ums.vo.SmsDataVO;
|
||||
import com.eactive.eai.rms.ext.djb.ums.vo.StdHeadVO;
|
||||
@@ -39,12 +42,15 @@ public class UmsService {
|
||||
private static final String SYS_DVCD = "API"; //시스템코드
|
||||
private static final String BZWK_DVCD = "99"; //업무구분코드
|
||||
private static final String SUB_BZWK_CD = "9019"; //세부구분코드
|
||||
private static final String TEAM_DVCD = "00"; //팀구분코드 TODO: 변경해야함
|
||||
|
||||
|
||||
private final MonitoringPropertyService propertyService;
|
||||
private final MessageRequestService messageRequestService;
|
||||
|
||||
public UmsService(MonitoringPropertyService propertyService) {
|
||||
public UmsService(MonitoringPropertyService propertyService, MessageRequestService messageRequestService) {
|
||||
this.propertyService = propertyService;
|
||||
this.messageRequestService = messageRequestService;
|
||||
}
|
||||
|
||||
public boolean send(MessageRequest messageRequest) throws Exception {
|
||||
@@ -168,17 +174,16 @@ public class UmsService {
|
||||
|
||||
// 스윙챗 알림 데이터
|
||||
SwingDataVO swiDataVO = new SwingDataVO();
|
||||
swiDataVO.setNotiCode("CNO_FEP_HMP_0002");
|
||||
swiDataVO.setNotiCode("CNO_FEP_APIM_0002"); //0001:긴급, 0002:중요
|
||||
swiDataVO.setNotiTitle("알림");
|
||||
swiDataVO.setNotiContent(messageRequest.getMessage());
|
||||
|
||||
// 스윙챗 알림 데이터
|
||||
List<SwingReceiverDataVO> swiReDataVOList = new ArrayList<SwingReceiverDataVO>();
|
||||
SwingReceiverDataVO receiver = new SwingReceiverDataVO();
|
||||
receiver.setType("");
|
||||
receiver.setType("PSN");
|
||||
receiver.setCompanyCode("CB");
|
||||
receiver.setCode(messageRequest.getMessengerId());
|
||||
receiver.setPrdNm("");
|
||||
receiver.setCode(messageRequest.getMessengerId());
|
||||
swiReDataVOList.add(receiver);
|
||||
|
||||
swiDataVO.setReceiverInfo(swiReDataVOList);
|
||||
@@ -200,22 +205,22 @@ public class UmsService {
|
||||
// Sms 데이터
|
||||
SmsDataVO dataVO = new SmsDataVO();
|
||||
dataVO.setSnd_dman_dt(jsonData[0]);
|
||||
dataVO.setSnd_dman_id("");
|
||||
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
|
||||
dataVO.setSnd_dvcd("EAI");
|
||||
dataVO.setMsg_titl("");
|
||||
dataVO.setMsg_titl(StringUtil.chunkString(messageRequest.getSubject(), 40, false)); // MS949 40byte 초과시 절단
|
||||
dataVO.setMsg_ctnt(messageRequest.getMessage());
|
||||
dataVO.setRecvr_no(PhoneNumberUtil.digitsOnly(messageRequest.getPhone()));
|
||||
dataVO.setSndn_no("15880079"); //발신번호
|
||||
dataVO.setSnd_empno(""); //요청자
|
||||
dataVO.setSnd_emp_dprm_cd(""); //요청부서코드
|
||||
dataVO.setSnd_empno("APIM"); //요청자
|
||||
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.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
|
||||
dataVO.setAlmtk_snd_prfl_key(""); // 플러스친구아이디
|
||||
dataVO.setAlmtk_tmplt_cd(""); // 템플릿코드
|
||||
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key", "")); // 플러스친구아이디
|
||||
dataVO.setAlmtk_tmplt_cd(BZWK_DVCD + TEAM_DVCD + SUB_BZWK_CD + "001"); // 템플릿코드
|
||||
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
|
||||
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
|
||||
}
|
||||
@@ -243,26 +248,26 @@ public class UmsService {
|
||||
|
||||
// 이메일 데이터
|
||||
EmailDataVO dataVO = new EmailDataVO();
|
||||
dataVO.setUms_evnt_id(""); // UMS이벤트ID(필수)
|
||||
dataVO.setUms_evnt_id(""); // TODO: UMS이벤트ID(필수)
|
||||
dataVO.setSnd_dman_dt(jsonData[0]);
|
||||
dataVO.setSnd_dman_id("");
|
||||
dataVO.setSnd_dman_sno("1");
|
||||
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
|
||||
dataVO.setCstno(""); // 고객번호(필수)
|
||||
dataVO.setSendr_emaddr(""); // 발신자 이메일
|
||||
dataVO.setSendr_nm(""); // 발신자 이름
|
||||
dataVO.setSndbk_emaddr(""); // 리턴 이메일
|
||||
dataVO.setCstno(""); // TODO: 고객번호(필수)
|
||||
dataVO.setSendr_emaddr(""); // TODO: 발신자 이메일
|
||||
dataVO.setSendr_nm(""); // TODO: 발신자 이름
|
||||
dataVO.setSndbk_emaddr(""); // TODO: 리턴 이메일
|
||||
dataVO.setRecvr_emaddr(messageRequest.getEmail()); // 수신자 이메일(필수)
|
||||
dataVO.setRecvr_nm(messageRequest.getUsername()); // 수신자명(필수)
|
||||
dataVO.setTmplt_mpng_info(messageRequest.getMessage()); // 탬플릿매핑정보
|
||||
dataVO.setTmplt_mpng_rptt_info(""); // 탬플릿매핑반복정보
|
||||
dataVO.setSnd_empno(""); // 발신자ID(필수)
|
||||
dataVO.setSnd_emp_dprm_cd(""); // 발신자부서코드(필수)
|
||||
dataVO.setSnd_empno("APIM"); // 발신자ID(필수)
|
||||
dataVO.setSnd_emp_dprm_cd("132"); // 발신자부서코드(필수)
|
||||
dataVO.setBzwk_cd(BZWK_DVCD); // 업무구분코드
|
||||
dataVO.setSub_bzwk_cd(SUB_BZWK_CD); // 업무세부코드
|
||||
dataVO.setSys_dvcd(SYS_DVCD);
|
||||
dataVO.setTx_dt(jsonData[0]); //거래일자
|
||||
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
|
||||
dataVO.setTx_dt(jsonData[0]); // 거래일자
|
||||
dataVO.setTx_tktm(jsonData[1].substring(0,6)); // 거래시간
|
||||
|
||||
List<EmailDataVO> dataList = new ArrayList<EmailDataVO>();
|
||||
dataList.add(dataVO);
|
||||
@@ -334,4 +339,24 @@ public class UmsService {
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
private String getSndDmanId(String messageType) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
|
||||
String yyyymmdd = sdf.format(new Date());
|
||||
|
||||
String uuid = SYS_DVCD + BZWK_DVCD + SUB_BZWK_CD + yyyymmdd + "O" ;
|
||||
|
||||
if ("SMS".equals(messageType)) {
|
||||
uuid += "S";
|
||||
} else if ("LMS".equals(messageType)) {
|
||||
uuid += "L";
|
||||
} else if ("KAKAO".equals(messageType)) {
|
||||
uuid += "K";
|
||||
}
|
||||
|
||||
long seq = messageRequestService.countSmsKakaoRequestsOnDate(LocalDate.now()) + 1;
|
||||
uuid += String.format("%014d", seq);
|
||||
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.UserServiceTypeService;
|
||||
import com.eactive.eai.rms.onl.apim.masking.MaskingUtils;
|
||||
import com.eactive.ext.kjb.common.KjbProperty;
|
||||
import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
||||
import com.eactive.ext.kjb.sso.KjbSsoModule;
|
||||
//import com.eactive.ext.kjb.common.KjbProperty;
|
||||
//import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
||||
//import com.eactive.ext.kjb.sso.KjbSsoModule;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -52,108 +52,108 @@ public class SsoController implements InterceptorSkipController {
|
||||
this.mainController = mainController;
|
||||
}
|
||||
|
||||
private KjbSsoModule getSsoModule() {
|
||||
return KjbSsoModule.getInstance();
|
||||
}
|
||||
// private KjbSsoModule getSsoModule() {
|
||||
// return KjbSsoModule.getInstance();
|
||||
// }
|
||||
//
|
||||
// private KjbProperty getProperty() {
|
||||
// return KjbPropertyHolder.get();
|
||||
// }
|
||||
|
||||
private KjbProperty getProperty() {
|
||||
return KjbPropertyHolder.get();
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/sso/login.do")
|
||||
public String login(HttpServletRequest req, HttpServletResponse res,
|
||||
@RequestParam(name = "UURL", required = false) String uurl) throws Exception {
|
||||
HttpSession session = req.getSession();
|
||||
String ssoId = getSsoModule().getSsoId(req);
|
||||
|
||||
log.debug("ssoId : {}", ssoId);
|
||||
log.debug("uurl : {}", uurl);
|
||||
|
||||
// 디버깅용 쿠키 목록 출력
|
||||
if (log.isDebugEnabled()) {
|
||||
Cookie[] cookies = req.getCookies();
|
||||
if (cookies == null || cookies.length == 0) {
|
||||
log.debug("No cookies");
|
||||
} 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 (ssoId == null) {
|
||||
getSsoModule().goLoginPage(res, uurl);
|
||||
return null;
|
||||
} else {
|
||||
// EAM 세션 검증
|
||||
String retCode = getSsoModule().getEamSessionCheck(req, res);
|
||||
log.debug("retCode : {}", retCode);
|
||||
|
||||
log.debug("Property - sso.ignore_validation = {}", getProperty().isSsoIgnoreValidation());
|
||||
if (!getProperty().isSsoIgnoreValidation() && !"0".equalsIgnoreCase(retCode)) {
|
||||
// SSO 검증 실패 시 에러 메시지 설정 및 로그인 페이지로 이동
|
||||
log.warn("SSO 검증 실패 - retCode: {}, ssoId: {}", retCode, ssoId);
|
||||
session.setAttribute("resultMsg", "SSO 로그인을 실패 하였습니다. (오류코드: " + retCode + ") 다른 로그인 방식을 시도 하세요");
|
||||
return "redirect:/loginForm.do";
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(ssoId)) {
|
||||
session.setAttribute("resultMsg", "SSO 인증 정보를 획득하지 못했습니다.");
|
||||
return "redirect:/loginForm.do";
|
||||
}
|
||||
|
||||
if (getProperty().isSsoSyncInfo()) {
|
||||
// 기본 정보
|
||||
Properties info = getSsoModule().getUserInfos(ssoId);
|
||||
// 확장 정보
|
||||
Properties extendedInfo = getSsoModule().getUserExFields(ssoId);
|
||||
|
||||
// 필수 필드 검증
|
||||
try {
|
||||
validateRequiredFields(info, extendedInfo);
|
||||
} 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;
|
||||
|
||||
log.info("SSO 로그인 시도 - userId: {}", userId);
|
||||
log.debug("SSO 사용자 정보 - userId: {}, name: {}, phone: {}, email: {}",
|
||||
userId,
|
||||
MaskingUtils.maskName(name),
|
||||
MaskingUtils.maskPhoneNumber(cellPhoneNo),
|
||||
MaskingUtils.maskEmailId(email));
|
||||
|
||||
// 사용자 생성 또는 업데이트
|
||||
UserInfo userInfo = findOrCreateUser(info, extendedInfo);
|
||||
|
||||
// 로그인 세션 설정 (MainController 위임)
|
||||
log.info("SSO 로그인 성공 - userId: {}", userId);
|
||||
return mainController.setupSsoLoginSession(req, userInfo);
|
||||
} else {
|
||||
// 동기화 비활성화 모드: 기존 사용자는 업데이트 없이, 신규 사용자는 최소 정보로 생성
|
||||
log.info("SSO 동기화 비활성화 모드로 로그인 처리 - ssoId: {}", ssoId);
|
||||
|
||||
UserInfo userInfo = findOrCreateUserWithoutSync(ssoId);
|
||||
|
||||
log.info("SSO 로그인 성공 (동기화 비활성화 모드) - userId: {}", ssoId);
|
||||
return mainController.setupSsoLoginSession(req, userInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
// @RequestMapping(value = "/sso/login.do")
|
||||
// public String login(HttpServletRequest req, HttpServletResponse res,
|
||||
// @RequestParam(name = "UURL", required = false) String uurl) throws Exception {
|
||||
// HttpSession session = req.getSession();
|
||||
// String ssoId = getSsoModule().getSsoId(req);
|
||||
//
|
||||
// log.debug("ssoId : {}", ssoId);
|
||||
// log.debug("uurl : {}", uurl);
|
||||
//
|
||||
// // 디버깅용 쿠키 목록 출력
|
||||
// if (log.isDebugEnabled()) {
|
||||
// Cookie[] cookies = req.getCookies();
|
||||
// if (cookies == null || cookies.length == 0) {
|
||||
// log.debug("No cookies");
|
||||
// } 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 (ssoId == null) {
|
||||
// getSsoModule().goLoginPage(res, uurl);
|
||||
// return null;
|
||||
// } else {
|
||||
// // EAM 세션 검증
|
||||
// String retCode = getSsoModule().getEamSessionCheck(req, res);
|
||||
// log.debug("retCode : {}", retCode);
|
||||
//
|
||||
// log.debug("Property - sso.ignore_validation = {}", getProperty().isSsoIgnoreValidation());
|
||||
// if (!getProperty().isSsoIgnoreValidation() && !"0".equalsIgnoreCase(retCode)) {
|
||||
// // SSO 검증 실패 시 에러 메시지 설정 및 로그인 페이지로 이동
|
||||
// log.warn("SSO 검증 실패 - retCode: {}, ssoId: {}", retCode, ssoId);
|
||||
// session.setAttribute("resultMsg", "SSO 로그인을 실패 하였습니다. (오류코드: " + retCode + ") 다른 로그인 방식을 시도 하세요");
|
||||
// return "redirect:/loginForm.do";
|
||||
// }
|
||||
//
|
||||
// if (StringUtils.isEmpty(ssoId)) {
|
||||
// session.setAttribute("resultMsg", "SSO 인증 정보를 획득하지 못했습니다.");
|
||||
// return "redirect:/loginForm.do";
|
||||
// }
|
||||
//
|
||||
// if (getProperty().isSsoSyncInfo()) {
|
||||
// // 기본 정보
|
||||
// Properties info = getSsoModule().getUserInfos(ssoId);
|
||||
// // 확장 정보
|
||||
// Properties extendedInfo = getSsoModule().getUserExFields(ssoId);
|
||||
//
|
||||
// // 필수 필드 검증
|
||||
// try {
|
||||
// validateRequiredFields(info, extendedInfo);
|
||||
// } 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;
|
||||
//
|
||||
// log.info("SSO 로그인 시도 - userId: {}", userId);
|
||||
// log.debug("SSO 사용자 정보 - userId: {}, name: {}, phone: {}, email: {}",
|
||||
// userId,
|
||||
// MaskingUtils.maskName(name),
|
||||
// MaskingUtils.maskPhoneNumber(cellPhoneNo),
|
||||
// MaskingUtils.maskEmailId(email));
|
||||
//
|
||||
// // 사용자 생성 또는 업데이트
|
||||
// UserInfo userInfo = findOrCreateUser(info, extendedInfo);
|
||||
//
|
||||
// // 로그인 세션 설정 (MainController 위임)
|
||||
// log.info("SSO 로그인 성공 - userId: {}", userId);
|
||||
// return mainController.setupSsoLoginSession(req, userInfo);
|
||||
// } else {
|
||||
// // 동기화 비활성화 모드: 기존 사용자는 업데이트 없이, 신규 사용자는 최소 정보로 생성
|
||||
// log.info("SSO 동기화 비활성화 모드로 로그인 처리 - ssoId: {}", ssoId);
|
||||
//
|
||||
// UserInfo userInfo = findOrCreateUserWithoutSync(ssoId);
|
||||
//
|
||||
// log.info("SSO 로그인 성공 (동기화 비활성화 모드) - userId: {}", ssoId);
|
||||
// return mainController.setupSsoLoginSession(req, userInfo);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 필수 필드 검증
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.eactive.eai.rms.common.util.CommonUtil;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricJpaDAO;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricService;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricVO;
|
||||
import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
||||
//import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
@@ -328,7 +328,7 @@ public class ObpGwMetricManService extends BaseService {
|
||||
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING));
|
||||
|
||||
// KjbProperty에서 pageSize 읽기
|
||||
Integer pageSize = KjbPropertyHolder.get().getApiGwMetricPageSize();
|
||||
Integer pageSize = null; //KjbPropertyHolder.get().getApiGwMetricPageSize();
|
||||
if (pageSize == null) {
|
||||
pageSize = 10000; // 기본값 (null 안전 처리)
|
||||
}
|
||||
|
||||
@@ -37,4 +37,7 @@ public class PortalInquiryCommentUI {
|
||||
@DateTimeFormat(pattern = "yyyyMMddHHmmss")
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
/** 공개범위(ALL=공개/PRIVATE=비공개). */
|
||||
private String visibility;
|
||||
|
||||
}
|
||||
|
||||
+13
@@ -92,4 +92,17 @@ public class PortalInquiryManController extends BaseAnnotationController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// 공개범위 변경 — 감사 로그는 AuditLogInterceptor 가 자동 기록(auditpoints.dat 등록, auditReason 파라미터)
|
||||
@PostMapping(value = "/onl/apim/portalinquiry/portalInquiryMan.json", params = "cmd=VISIBILITY")
|
||||
public ResponseEntity<String> updateVisibility(String targetType, String id, String visibility, String auditReason) {
|
||||
portalInquiryManService.updateVisibility(targetType, id, visibility, auditReason);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/portalinquiry/portalInquiryMan.json", params = "cmd=INCREMENT_VIEW")
|
||||
public ResponseEntity<Void> incrementView(String id) {
|
||||
portalInquiryManService.incrementViewCount(id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
||||
import com.eactive.apim.portal.qna.entity.VisibilityScope;
|
||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||
import com.eactive.eai.rms.common.base.BaseService;
|
||||
import com.eactive.eai.rms.common.exception.IntegrationException;
|
||||
@@ -44,6 +45,7 @@ public class PortalInquiryManService extends BaseService {
|
||||
public static final String REVIEWING = "REVIEWING";
|
||||
public static final String RESPONDED = "RESPONDED";
|
||||
public static final String CLOSED = "CLOSED";
|
||||
public static final String TARGET_COMMENT = "COMMENT";
|
||||
|
||||
|
||||
public PortalInquiryManService(PortalInquiryService portalInquiryService,
|
||||
@@ -235,4 +237,41 @@ public class PortalInquiryManService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 조회수 증가 (JSP sessionStorage dedup 통과 시 호출). 관리자는 공개범위 무관 전체 접근. */
|
||||
public void incrementViewCount(String id) {
|
||||
Inquiry inquiry = portalInquiryService.getById(id);
|
||||
inquiry.increaseViewCount();
|
||||
portalInquiryService.save(inquiry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물/댓글 공개범위 변경. 사유(reason) 필수.
|
||||
* 변경 이력은 eapim-admin 공통 감사 로그(AuditLogInterceptor)가 자동 기록한다
|
||||
* (auditpoints.dat 의 PortalInquiryManController_..._VISIBILITY, serviceType=APIGW, auditReason 파라미터).
|
||||
*
|
||||
* @param targetType INQUIRY 또는 COMMENT
|
||||
* @param id 대상 게시물/댓글 ID
|
||||
* @param newVisibility 변경할 공개범위(ALL/ORG/PRIVATE)
|
||||
* @param reason 변경 사유(필수)
|
||||
*/
|
||||
public void updateVisibility(String targetType, String id, String newVisibility, String reason) {
|
||||
if (StringUtils.isBlank(reason)) {
|
||||
throw new IntegrationException("공개범위 변경 사유를 입력해주세요.");
|
||||
}
|
||||
VisibilityScope newScope = VisibilityScope.fromString(newVisibility, null);
|
||||
if (newScope == null) {
|
||||
throw new IntegrationException("유효하지 않은 공개범위입니다.");
|
||||
}
|
||||
|
||||
if (TARGET_COMMENT.equalsIgnoreCase(targetType)) {
|
||||
InquiryComment comment = portalInquiryCommentService.getById(id);
|
||||
comment.setVisibility(newScope);
|
||||
portalInquiryCommentService.save(comment);
|
||||
} else {
|
||||
Inquiry inquiry = portalInquiryService.getById(id);
|
||||
inquiry.setVisibility(newScope);
|
||||
portalInquiryService.save(inquiry);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,4 +57,10 @@ public class PortalInquiryUI {
|
||||
private String orgName;
|
||||
|
||||
private int commentCount;
|
||||
|
||||
/** 공개범위(ALL/ORG/PRIVATE). */
|
||||
private String visibility;
|
||||
|
||||
/** 조회수. */
|
||||
private long viewCount;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public interface PortalPropertyMapper extends GenericMapper<PropertyUI, PortalPr
|
||||
@Mapping(source = "prptyGroupName", target = "id.propertyGroupName")
|
||||
@Mapping(source = "prptyName", target = "id.propertyName")
|
||||
@Mapping(source = "prpty2Val", target = "propertyValue")
|
||||
@Mapping(source = "prptyDesc", target = "propertyDesc")
|
||||
@Override
|
||||
PortalProperty toEntity(PropertyUI vo);
|
||||
|
||||
@@ -21,6 +22,7 @@ public interface PortalPropertyMapper extends GenericMapper<PropertyUI, PortalPr
|
||||
@Mapping(source ="id.propertyGroupName", target = "prptyGroupName")
|
||||
@Mapping(source ="id.propertyName", target = "prptyName")
|
||||
@Mapping(source ="propertyValue", target = "prpty2Val")
|
||||
@Mapping(source ="propertyDesc", target = "prptyDesc")
|
||||
@InheritInverseConfiguration
|
||||
@Override
|
||||
PropertyUI toVo(PortalProperty entity);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.eactive.eai.rms.onl.apim.service;
|
||||
|
||||
import com.eactive.eai.util.IpUtil;
|
||||
import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.rms.ext.djb.common.DjbPropertyHolder;
|
||||
import com.eactive.eai.util.IpUtil;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* IP 화이트리스트 인증 서비스
|
||||
*
|
||||
@@ -113,7 +115,7 @@ public class IpAuthenticationService {
|
||||
*/
|
||||
private String loadWhitelistFromDb() {
|
||||
try {
|
||||
String whitelist = KjbPropertyHolder.get().getApiAllowIpList();
|
||||
String whitelist = DjbPropertyHolder.get().getApiAllowIpList();
|
||||
if (StringUtils.isBlank(whitelist)) {
|
||||
log.warn("IP whitelist property is empty");
|
||||
return "";
|
||||
|
||||
@@ -15,4 +15,7 @@ public class PropertyUI {
|
||||
|
||||
@JsonProperty("PRPTY2VAL")
|
||||
private String prpty2Val;
|
||||
|
||||
@JsonProperty("PRPTYDESC")
|
||||
private String prptyDesc;
|
||||
}
|
||||
|
||||
+12
-10
@@ -19,7 +19,9 @@ import com.eactive.eai.agent.command.CommonCommand;
|
||||
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
|
||||
import com.eactive.eai.rms.common.combo.ComboService;
|
||||
import com.eactive.eai.rms.common.combo.ComboVo;
|
||||
import com.eactive.eai.rms.common.util.CommonUtil;
|
||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||
import com.eactive.eai.rms.common.vo.SimpleResponse;
|
||||
|
||||
@Controller
|
||||
public class InflowClientControlManController extends OnlBaseAnnotationController {
|
||||
@@ -55,22 +57,22 @@ public class InflowClientControlManController extends OnlBaseAnnotationControlle
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=UPDATE")
|
||||
public String save(HttpServletRequest request, HttpServletResponse response, InflowControlManUI ui)
|
||||
public ResponseEntity<SimpleResponse> save(HttpServletRequest request, HttpServletResponse response, InflowControlManUI ui)
|
||||
throws Exception {
|
||||
service.mergeClient(ui);
|
||||
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowClientControlCommand",
|
||||
ui.getName());
|
||||
agentUtilService.broadcast(command);
|
||||
return null;
|
||||
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowClientControlCommand", ui.getName());
|
||||
HashMap<String, Object> returnMap = agentUtilService.broadcast(command);
|
||||
SimpleResponse simpleResponse = SimpleResponse.of(CommonUtil.getMapString(returnMap, true));
|
||||
return ResponseEntity.ok(simpleResponse);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=DELETE")
|
||||
public String delete(HttpServletRequest request, HttpServletResponse response, String name) throws Exception {
|
||||
public ResponseEntity<SimpleResponse> delete(HttpServletRequest request, HttpServletResponse response, String name) throws Exception {
|
||||
service.deleteClient(name);
|
||||
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.RemoveInflowClientControlCommand",
|
||||
name);
|
||||
agentUtilService.broadcast(command);
|
||||
return null;
|
||||
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.RemoveInflowClientControlCommand", name);
|
||||
HashMap<String, Object> returnMap = agentUtilService.broadcast(command);
|
||||
SimpleResponse simpleResponse = SimpleResponse.of(CommonUtil.getMapString(returnMap, true));
|
||||
return ResponseEntity.ok(simpleResponse);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=LIST_INIT_COMBO")
|
||||
|
||||
+17
-3
@@ -1,5 +1,6 @@
|
||||
package com.eactive.eai.rms.onl.manage.inflow.inflow;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -16,6 +17,7 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.inflow.InflowControl;
|
||||
import com.eactive.eai.data.entity.onl.inflow.InflowControlId;
|
||||
@@ -72,6 +74,9 @@ public class InflowControlManService extends BaseService {
|
||||
} else {
|
||||
entity = mapper.toEntity(ui);
|
||||
}
|
||||
if (entity.getThreshold() == null || StringUtils.isEmpty(entity.getThresholdtimeunit())) {
|
||||
entity.setThreshold(0);
|
||||
}
|
||||
service.save(entity);
|
||||
}
|
||||
|
||||
@@ -102,6 +107,9 @@ public class InflowControlManService extends BaseService {
|
||||
} else {
|
||||
entity = mapper.toEntity(ui);
|
||||
}
|
||||
if (entity.getThreshold() == null || StringUtils.isEmpty(entity.getThresholdtimeunit())) {
|
||||
entity.setThreshold(0);
|
||||
}
|
||||
service.save(entity);
|
||||
}
|
||||
|
||||
@@ -133,6 +141,9 @@ public class InflowControlManService extends BaseService {
|
||||
} else {
|
||||
entity = mapper.toEntity(ui);
|
||||
}
|
||||
if (entity.getThreshold() == null || StringUtils.isEmpty(entity.getThresholdtimeunit())) {
|
||||
entity.setThreshold(0);
|
||||
}
|
||||
service.save(entity);
|
||||
}
|
||||
|
||||
@@ -172,9 +183,12 @@ public class InflowControlManService extends BaseService {
|
||||
serverStatus.put("serverPort", serverPort);
|
||||
|
||||
try {
|
||||
String url = String.format("http://%s:%s/manage/inflow/%s/%s/bucket-status",
|
||||
serverIp, serverPort, target, targetId);
|
||||
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
|
||||
URI uri = UriComponentsBuilder.fromHttpUrl(String.format("http://%s:%s/manage/inflow", serverIp, serverPort))
|
||||
.pathSegment(target, targetId, "bucket-status")
|
||||
.build()
|
||||
.encode()
|
||||
.toUri();
|
||||
ResponseEntity<Map> response = restTemplate.getForEntity(uri, Map.class);
|
||||
|
||||
if (response.getBody() != null && Boolean.TRUE.equals(response.getBody().get("success"))) {
|
||||
serverStatus.put("status", "online");
|
||||
|
||||
@@ -50,7 +50,7 @@ public class ApiSpecController extends OnlBaseAnnotationController {
|
||||
return "/onl/transaction/apim/apiSpecManPopup";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/transaction/apim/apiSpecManPopup.view", params = "cmd=POPUP_LIST")
|
||||
@GetMapping(value = "/onl/transaction/apim/apiSpecMan.view", params = "cmd=POPUP_LIST")
|
||||
public String detailOrgPopupView() {
|
||||
return "/onl/transaction/apim/apiSpecManDisplayOrgPopup";
|
||||
}
|
||||
|
||||
@@ -448,14 +448,24 @@ public class ApiSpecManService {
|
||||
// 헤더/바디 분리
|
||||
RequestComponents components = separateHeaderAndBody(requestLayout.getLayoutItems());
|
||||
|
||||
// 헤더 파라미터 처리
|
||||
if (!components.headerItems.isEmpty()) {
|
||||
boolean bodyRequired = isRequestBodyRequired(httpMethod);
|
||||
|
||||
// 헤더 파라미터 + (POST/PUT 이 아니면) 요청 바디 항목을 쿼리스트링 파라미터로 함께 담는다.
|
||||
boolean hasHeader = !components.headerItems.isEmpty();
|
||||
boolean queryFromBody = !bodyRequired && !components.bodyItems.isEmpty();
|
||||
if (hasHeader || queryFromBody) {
|
||||
ArrayNode parameters = operation.putArray("parameters");
|
||||
generateHeaderParameters(parameters, components.headerItems);
|
||||
if (hasHeader) {
|
||||
generateHeaderParameters(parameters, components.headerItems);
|
||||
}
|
||||
// GET/DELETE 등 바디 미사용 메소드: 요청 레이아웃 바디 항목 → 쿼리 파라미터(in=query)
|
||||
if (queryFromBody) {
|
||||
generateQueryParameters(parameters, components.bodyItems);
|
||||
}
|
||||
}
|
||||
|
||||
// Body 처리 (POST, PUT 메소드인 경우)
|
||||
if (isRequestBodyRequired(httpMethod)) {
|
||||
if (bodyRequired) {
|
||||
ObjectNode requestBody = operation.putObject("requestBody");
|
||||
requestBody.put("required", true);
|
||||
ObjectNode content = requestBody.putObject("content");
|
||||
@@ -561,6 +571,35 @@ public class ApiSpecManService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET/DELETE 등 바디 미사용 메소드: 요청 레이아웃 바디 항목을 쿼리스트링 파라미터(in=query)로 매핑.
|
||||
* 스칼라 FIELD 만 대상(GROUP/GRID 중첩 객체·배열은 쿼리스트링에 부적합하여 제외), root(serno 0)는 스킵.
|
||||
*/
|
||||
private void generateQueryParameters(ArrayNode parameters, List<LayoutItemUI> bodyItems) {
|
||||
for (LayoutItemUI item : bodyItems) {
|
||||
if (item.getLoutItemSerno() != null && item.getLoutItemSerno() == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!"FIELD".equalsIgnoreCase(item.getLoutItemType())) {
|
||||
continue;
|
||||
}
|
||||
ObjectNode parameter = parameters.addObject();
|
||||
parameter.put("name", item.getLoutItemName());
|
||||
parameter.put("in", "query");
|
||||
if (StringUtils.isNotEmpty(item.getLoutItemDesc())) {
|
||||
parameter.put("description", item.getLoutItemDesc());
|
||||
}
|
||||
parameter.put("required", false); // 기본값 false, 필요시 사용자가 변경
|
||||
|
||||
ObjectNode schema = parameter.putObject("schema");
|
||||
schema.put("type", mapSwaggerType(item.getLoutItemDataType()));
|
||||
int length = item.getLoutItemLength();
|
||||
if (length > 0) {
|
||||
schema.put("maxLength", length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void generateResponseHeaders(ObjectNode headers, List<LayoutItemUI> headerItems) {
|
||||
for (LayoutItemUI item : headerItems) {
|
||||
if ("FIELD".equalsIgnoreCase(item.getLoutItemType())) {
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
||||
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
|
||||
import com.eactive.eai.rms.data.entity.onl.eaimsg.EAIMessageService;
|
||||
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutUI;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.apigroup.ApiGroupService;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
/**
|
||||
* 신규 OpenAPI Spec 6단계 편집 마법사 컨트롤러.
|
||||
*
|
||||
* apiInterfaceMan 상세의 "OpenAPI Spec" 버튼이 새 탭(_blank)으로 여는 페이지 및 마법사가 호출하는
|
||||
* cmd API 를 제공한다. 저장은 기존 컬럼 + testbedSpec(OpenAPI JSON)에 매핑(스키마 무변경, 경량).
|
||||
*
|
||||
* (MVC 컨트롤러는 springapp-servlet.xml 의 com.eactive.eai.rms 스캔 범위에서만 등록되므로 본 컨트롤러는
|
||||
* 해당 패키지에 둔다. 기존 apiSpecMan.view(v1)와 URL 을 분리해 공존한다.)
|
||||
*/
|
||||
@Controller
|
||||
public class DjbApiSpecController extends OnlBaseAnnotationController {
|
||||
|
||||
@Autowired
|
||||
private ApiSpecManService apiSpecManService;
|
||||
|
||||
@Autowired
|
||||
private ApiInterfaceService apiInterfaceService;
|
||||
|
||||
@Autowired
|
||||
private EAIMessageService eaiMessageService;
|
||||
|
||||
@Autowired
|
||||
private DjbApiSpecSwaggerEnricher swaggerEnricher;
|
||||
|
||||
@Autowired
|
||||
private DjbApiSpecLayoutMergeService layoutMergeService;
|
||||
|
||||
@Autowired
|
||||
private PortalPropertyService portalPropertyService;
|
||||
|
||||
@Autowired
|
||||
private ApiGroupService apiGroupService;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// GW base URL 단일 기준 = 포탈 DjbTestbedGatewayProperty 의 djb.gateway.base-url (PTL_PROPERTY 그룹 'Portal').
|
||||
// 별도 swagger.gw.address 프로퍼티는 두지 않는다.
|
||||
private static final String GW_BASE_URL_KEY = "djb.gateway.base-url";
|
||||
private static final String GW_BASE_URL_DEFAULT = "PortalMock";
|
||||
// "PortalMock" 은 포탈이 요청 origin 으로 치환하는 sentinel 이라 admin 미리보기용 서버 주소로는 부적합 → 대체값 사용.
|
||||
private static final String GW_PREVIEW_FALLBACK = "http://127.0.0.1:39310";
|
||||
|
||||
// ── 진입 ────────────────────────────────────────────────
|
||||
@GetMapping(value = "/onl/transaction/apim/djbApiSpecMan.view", params = "cmd=DETAIL")
|
||||
public String viewDetail(Model model) {
|
||||
// GW base URL (djb.gateway.base-url, 그룹 'Portal') — 없으면 기본값으로 생성. DjbTestbedGatewayProperty 와 동일 키.
|
||||
String gw = portalPropertyService.getOrCreateProperty(
|
||||
"Portal", GW_BASE_URL_KEY, GW_BASE_URL_DEFAULT, "GW base URL (테스트베드 공통)");
|
||||
if (StringUtils.isBlank(gw) || GW_BASE_URL_DEFAULT.equalsIgnoreCase(gw.trim())) {
|
||||
gw = GW_PREVIEW_FALLBACK; // PortalMock → admin 미리보기용 구체 주소로 대체
|
||||
}
|
||||
model.addAttribute("gwAddress", gw.trim());
|
||||
return "/onl/transaction/apim/djbApiSpecManPopup";
|
||||
}
|
||||
|
||||
// API 그룹 선택 팝업(iframe 모달 대상). 그룹 목록 그리드는 apiGroupMan.json?cmd=LIST 재사용.
|
||||
@GetMapping(value = "/onl/transaction/apim/djbApiSpecMan.view", params = "cmd=GROUP_POPUP")
|
||||
public String viewGroupPopup() {
|
||||
return "/onl/transaction/apim/djbApiGroupPopup";
|
||||
}
|
||||
|
||||
// ── 조회: 저장 spec 우선 + authtype securityScheme 주입 ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=DETAIL_SPEC")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> detailSpec(String eaiSvcName, String regen) throws Exception {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (StringUtils.isBlank(eaiSvcName)) {
|
||||
result.put("status", "fail");
|
||||
result.put("errorMsg", "eaiSvcName 이 없습니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
// regen=true 이면 저장본 무시하고 레이아웃 기반으로 강제 재생성 (자동 생성/초기화)
|
||||
boolean force = "true".equalsIgnoreCase(regen);
|
||||
String spec;
|
||||
String source;
|
||||
ApiSpecInfoUI saved = force ? null : apiSpecManService.selectDetail(eaiSvcName);
|
||||
if (!force && saved != null && StringUtils.isNotBlank(saved.getTestbedSpec())) {
|
||||
spec = saved.getTestbedSpec();
|
||||
source = "saved";
|
||||
} else {
|
||||
spec = generateSpec(eaiSvcName);
|
||||
source = force ? "regenerated" : "generated";
|
||||
}
|
||||
|
||||
String authtype = eaiMessageService.findById(eaiSvcName)
|
||||
.map(EAIMessageEntity::getAuthtype)
|
||||
.orElse(null);
|
||||
String enriched = swaggerEnricher.enrich(spec, authtype);
|
||||
|
||||
result.put("testbedSpec", enriched);
|
||||
result.put("source", source);
|
||||
result.put("authType", authtype == null ? "" : authtype);
|
||||
// API 그룹 소속(조인테이블 ApiGroupApi) — 저장 여부와 무관하게 현재 소속 반환
|
||||
try {
|
||||
ArrayNode groups = objectMapper.createArrayNode();
|
||||
for (ApiGroup g : apiGroupService.findByApiId(eaiSvcName)) {
|
||||
ObjectNode gn = objectMapper.createObjectNode();
|
||||
gn.put("id", g.getId());
|
||||
gn.put("groupName", g.getGroupName());
|
||||
groups.add(gn);
|
||||
}
|
||||
result.put("apiGroups", objectMapper.writeValueAsString(groups));
|
||||
} catch (Exception e) {
|
||||
logger.warn("API 그룹 조회 실패: " + eaiSvcName, e);
|
||||
}
|
||||
// 저장본이면 폼 복원용 기본정보/공개설정도 함께 반환
|
||||
if (saved != null) {
|
||||
putIfNotNull(result, "apiName", saved.getApiName());
|
||||
putIfNotNull(result, "apiSimpleDescription", saved.getApiSimpleDescription());
|
||||
putIfNotNull(result, "apiUrl", saved.getApiUrl());
|
||||
putIfNotNull(result, "apiMethod", saved.getApiMethod());
|
||||
putIfNotNull(result, "apiContentType", saved.getApiContentType());
|
||||
putIfNotNull(result, "description", saved.getDescription());
|
||||
putIfNotNull(result, "apiRequestSpec", saved.getApiRequestSpec());
|
||||
putIfNotNull(result, "apiResponseSpec", saved.getApiResponseSpec());
|
||||
putIfNotNull(result, "sampleRequest", saved.getSampleRequest());
|
||||
putIfNotNull(result, "sampleResponse", saved.getSampleResponse());
|
||||
putIfNotNull(result, "responseType", saved.getResponseType());
|
||||
putIfNotNull(result, "mockUrl", saved.getMockUrl());
|
||||
putIfNotNull(result, "displayYn", saved.getDisplayYn());
|
||||
putIfNotNull(result, "displayRoleCode", saved.getDisplayRoleCode());
|
||||
putIfNotNull(result, "displayOrg", saved.getDisplayOrg());
|
||||
}
|
||||
// 예제/설명자료(요청·응답 메시지 스펙 HTML)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치.
|
||||
// 저장본이면 위 saved 블록에서 저장값을 이미 반환.
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 저장: 마법사 폼 → ApiSpecInfoUI → apiSpecManService.save (기존 재사용) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=SAVE")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> save(ApiSpecInfoUI apiSpecInfoUI, String apiGroupIds) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (apiSpecInfoUI == null || StringUtils.isBlank(apiSpecInfoUI.getApiId())) {
|
||||
result.put("status", "fail");
|
||||
result.put("errorMsg", "API ID 가 없습니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
// CrossScriptingFilter(RequestWrapper.cleanXSS)가 요청 파라미터의 특수문자를 엔티티로 치환
|
||||
// ('('→( ')'→) '"'→" 등)하므로, 저장 전 모든 자유텍스트/JSON 필드를 언이스케이프해 원복한다.
|
||||
// (JSON 필드는 특히 '"'→" 로 깨지므로 필수)
|
||||
apiSpecInfoUI.setApiName(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiName()));
|
||||
apiSpecInfoUI.setApiSimpleDescription(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiSimpleDescription()));
|
||||
apiSpecInfoUI.setApiUrl(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiUrl()));
|
||||
apiSpecInfoUI.setMockUrl(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getMockUrl()));
|
||||
apiSpecInfoUI.setApiRequestSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiRequestSpec()));
|
||||
apiSpecInfoUI.setApiResponseSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiResponseSpec()));
|
||||
apiSpecInfoUI.setSampleRequest(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getSampleRequest()));
|
||||
apiSpecInfoUI.setSampleResponse(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getSampleResponse()));
|
||||
apiSpecInfoUI.setDescription(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getDescription()));
|
||||
apiSpecInfoUI.setTestbedSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getTestbedSpec()));
|
||||
|
||||
apiSpecManService.save(apiSpecInfoUI);
|
||||
|
||||
// API 그룹 소속(조인테이블) 동기화 — apiGroupIds(CSV) 로 재설정
|
||||
try {
|
||||
java.util.List<String> gids = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(apiGroupIds)) {
|
||||
for (String g : apiGroupIds.split(",")) {
|
||||
if (StringUtils.isNotBlank(g)) {
|
||||
gids.add(g.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
apiGroupService.setApiGroupsForApi(apiSpecInfoUI.getApiId(), gids);
|
||||
} catch (Exception e) {
|
||||
logger.warn("API 그룹 동기화 실패: " + apiSpecInfoUI.getApiId(), e);
|
||||
}
|
||||
|
||||
result.put("status", "success");
|
||||
result.put("apiId", apiSpecInfoUI.getApiId());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 레이아웃 불러오기 (GW) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=LOAD_REQUEST_LAYOUT")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> loadRequestLayout(String eaiSvcName) throws Exception {
|
||||
return loadLayout(eaiSvcName, true);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=LOAD_RESPONSE_LAYOUT")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> loadResponseLayout(String eaiSvcName) throws Exception {
|
||||
return loadLayout(eaiSvcName, false);
|
||||
}
|
||||
|
||||
// ── HTML 테이블 생성 (기존 generateSpecTableFromLayout 재사용) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=GENERATE_HTML")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> generateHtml(String eaiSvcName, String layoutType) throws Exception {
|
||||
LayoutUI layout = resolveLayout(eaiSvcName, !"RESPONSE".equalsIgnoreCase(layoutType));
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("html", apiSpecManService.generateSpecTableFromLayout(layout));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 메시지 예제 생성 (기존 generateSampleDataFromLayout 재사용) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=GENERATE_SAMPLE")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> generateSample(String eaiSvcName, String layoutType) throws Exception {
|
||||
LayoutUI layout = resolveLayout(eaiSvcName, !"RESPONSE".equalsIgnoreCase(layoutType));
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("json", apiSpecManService.generateSampleDataFromLayout(layout));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 내부 헬퍼 ──────────────────────────────────────────
|
||||
private ResponseEntity<Map<String, Object>> loadLayout(String eaiSvcName, boolean request) throws Exception {
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
if (StringUtils.isBlank(eaiSvcName)) {
|
||||
res.put("layoutType", request ? "REQUEST" : "RESPONSE");
|
||||
res.put("fields", new ArrayList<>());
|
||||
warnings.add("eaiSvcName 이 없습니다.");
|
||||
res.put("warnings", warnings);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
LayoutUI layout = resolveLayout(eaiSvcName, request);
|
||||
if (layout == null || layout.getLayoutItems() == null || layout.getLayoutItems().isEmpty()) {
|
||||
warnings.add((request ? "요청" : "응답") + " 레이아웃이 없습니다.");
|
||||
}
|
||||
res.put("layoutType", request ? "REQUEST" : "RESPONSE");
|
||||
res.put("fields", layoutMergeService.toFields(layout, request ? "REQUEST" : "RESPONSE"));
|
||||
res.put("warnings", warnings);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
|
||||
private LayoutUI resolveLayout(String eaiSvcName, boolean request) throws Exception {
|
||||
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||
if (apiInterfaceUI == null) {
|
||||
return null;
|
||||
}
|
||||
String layoutName = request ? apiInterfaceUI.getInboundRequestLayout() : apiInterfaceUI.getInboundResponseLayout();
|
||||
return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 기반 자동 생성 + 자동생성 규칙(백엔드 단일 소스).
|
||||
* 규칙: title/tag/summary = API명(eaiSvcDesc), operationId = 인터페이스ID,
|
||||
* servers[0].url 을 path 앞에 붙이고 servers 제거(서버 선택 제거),
|
||||
* 요청/200 응답 예제를 레이아웃 샘플로 임베드.
|
||||
*/
|
||||
private String generateSpec(String eaiSvcName) throws Exception {
|
||||
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||
if (apiInterfaceUI == null) {
|
||||
throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
|
||||
}
|
||||
Map<String, String> inboundAdapterSpec = apiInterfaceService.getHttpAdapterInfo(apiInterfaceUI.getFromAdapter());
|
||||
if (inboundAdapterSpec == null) {
|
||||
inboundAdapterSpec = new HashMap<>();
|
||||
}
|
||||
String requestLayoutName = apiInterfaceUI.getInboundRequestLayout();
|
||||
String responseLayoutName = apiInterfaceUI.getInboundResponseLayout();
|
||||
LayoutUI requestLayoutUI = StringUtils.isNotEmpty(requestLayoutName)
|
||||
? apiSpecManService.selectLayoutUI(requestLayoutName) : null;
|
||||
LayoutUI responseLayoutUI = StringUtils.isNotEmpty(responseLayoutName)
|
||||
? apiSpecManService.selectLayoutUI(responseLayoutName) : null;
|
||||
|
||||
String baseSpec = apiSpecManService.generateSwaggerSpec(requestLayoutUI, responseLayoutUI, inboundAdapterSpec, apiInterfaceUI);
|
||||
|
||||
String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
|
||||
String reqSample = apiSpecManService.generateSampleDataFromLayout(requestLayoutUI);
|
||||
String resSample = apiSpecManService.generateSampleDataFromLayout(responseLayoutUI);
|
||||
String contentType = StringUtils.isNotBlank(apiInterfaceUI.getRestContentType()) ? apiInterfaceUI.getRestContentType() : "application/json";
|
||||
return applyAutoRules(baseSpec, apiName, eaiSvcName, reqSample, resSample, contentType);
|
||||
}
|
||||
|
||||
/** 자동생성 규칙 후처리(Jackson 트리 조작). 실패 시 원본 유지. */
|
||||
private String applyAutoRules(String specJson, String apiName, String eaiSvcName, String reqSample, String resSample, String contentType) {
|
||||
try {
|
||||
JsonNode parsed = objectMapper.readTree(specJson);
|
||||
if (!parsed.isObject()) {
|
||||
return specJson;
|
||||
}
|
||||
ObjectNode root = (ObjectNode) parsed;
|
||||
|
||||
root.put("openapi", "3.1.0"); // OpenAPI 3.1 방출(Swagger UI 5.x oas31 렌더 지원)
|
||||
getOrCreateObject(root, "info").put("title", apiName);
|
||||
|
||||
String tagName = "DJBank"; // 자동생성 태그명 고정
|
||||
ArrayNode tags = objectMapper.createArrayNode();
|
||||
ObjectNode tag = objectMapper.createObjectNode();
|
||||
tag.put("name", tagName);
|
||||
tag.put("description", apiName); // 태그 설명 = 간단설명(API명)
|
||||
tags.add(tag);
|
||||
root.set("tags", tags);
|
||||
|
||||
// 어댑터경로(servers[0].url = getHttpAdapterInfo urlPath)를 path 앞에 baking 하고 servers 제거.
|
||||
// 호스트(gw=djb.gateway.base-url / mock=Mock URL / sample=없음)는 프론트가 응답유형에 따라 서버로 붙인다.
|
||||
String adapterPath = "";
|
||||
JsonNode serversNode = root.get("servers");
|
||||
if (serversNode != null && serversNode.isArray() && serversNode.size() > 0 && serversNode.get(0).get("url") != null) {
|
||||
adapterPath = serversNode.get(0).get("url").asText("");
|
||||
}
|
||||
root.remove("servers");
|
||||
|
||||
JsonNode pathsNode = root.get("paths");
|
||||
if (pathsNode != null && pathsNode.isObject()) {
|
||||
ObjectNode paths = (ObjectNode) pathsNode;
|
||||
List<String> pathKeys = new ArrayList<>();
|
||||
Iterator<String> pit = paths.fieldNames();
|
||||
while (pit.hasNext()) {
|
||||
pathKeys.add(pit.next());
|
||||
}
|
||||
for (String pk : pathKeys) {
|
||||
JsonNode pathItem = paths.get(pk);
|
||||
if (pathItem != null && pathItem.isObject()) {
|
||||
List<String> methods = new ArrayList<>();
|
||||
Iterator<String> mit = pathItem.fieldNames();
|
||||
while (mit.hasNext()) {
|
||||
methods.add(mit.next());
|
||||
}
|
||||
for (String mk : methods) {
|
||||
JsonNode opNode = pathItem.get(mk);
|
||||
if (opNode != null && opNode.isObject()) {
|
||||
ObjectNode op = (ObjectNode) opNode;
|
||||
op.put("operationId", eaiSvcName);
|
||||
op.put("summary", apiName);
|
||||
ArrayNode opTags = objectMapper.createArrayNode();
|
||||
opTags.add(tagName);
|
||||
op.set("tags", opTags);
|
||||
// 예제(요청/응답 본문)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치. 여기선 임베드하지 않음.
|
||||
markGwOnOperation(op);
|
||||
addResponseHeader(op, "200", "Content-Type", contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
String newKey = adapterPath.isEmpty() ? pk
|
||||
: (adapterPath.replaceAll("/+$", "") + (pk.startsWith("/") ? "" : "/") + pk);
|
||||
if (!newKey.equals(pk)) {
|
||||
paths.set(newKey, pathItem);
|
||||
paths.remove(pk);
|
||||
}
|
||||
}
|
||||
}
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (Exception e) {
|
||||
logger.error("applyAutoRules 실패, 원본 spec 유지", e);
|
||||
return specJson;
|
||||
}
|
||||
}
|
||||
|
||||
private void setContentExample(ObjectNode op, String sampleJson) {
|
||||
if (StringUtils.isBlank(sampleJson)) {
|
||||
return;
|
||||
}
|
||||
JsonNode body = op.get("requestBody");
|
||||
if (body == null || !body.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode content = body.get("content");
|
||||
if (content == null || !content.isObject()) {
|
||||
return;
|
||||
}
|
||||
Iterator<String> mts = content.fieldNames();
|
||||
if (!mts.hasNext()) {
|
||||
return;
|
||||
}
|
||||
JsonNode mtNode = content.get(mts.next());
|
||||
if (mtNode != null && mtNode.isObject()) {
|
||||
((ObjectNode) mtNode).set("example", parseJsonOrText(sampleJson));
|
||||
}
|
||||
}
|
||||
|
||||
private void setResponseExample(ObjectNode op, String code, String sampleJson) {
|
||||
if (StringUtils.isBlank(sampleJson)) {
|
||||
return;
|
||||
}
|
||||
JsonNode resps = op.get("responses");
|
||||
if (resps == null || !resps.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode r = resps.get(code);
|
||||
if (r == null || !r.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode content = getOrCreateObject((ObjectNode) r, "content");
|
||||
ObjectNode appjson = getOrCreateObject(content, "application/json");
|
||||
appjson.set("example", parseJsonOrText(sampleJson));
|
||||
}
|
||||
|
||||
/** GW 유래 스키마 필드에 x-djb-gw 마커 부여(프론트에서 잠금 표시). */
|
||||
private void markGwOnOperation(ObjectNode op) {
|
||||
JsonNode body = op.get("requestBody");
|
||||
if (body != null && body.isObject()) {
|
||||
markGwInContent(body.get("content"));
|
||||
}
|
||||
JsonNode resps = op.get("responses");
|
||||
if (resps != null && resps.isObject()) {
|
||||
List<String> codes = new ArrayList<>();
|
||||
Iterator<String> it = resps.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
codes.add(it.next());
|
||||
}
|
||||
for (String c : codes) {
|
||||
JsonNode r = resps.get(c);
|
||||
if (r != null && r.isObject()) {
|
||||
markGwInContent(r.get("content"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markGwInContent(JsonNode content) {
|
||||
if (content == null || !content.isObject()) {
|
||||
return;
|
||||
}
|
||||
List<String> mts = new ArrayList<>();
|
||||
Iterator<String> it = content.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
mts.add(it.next());
|
||||
}
|
||||
for (String mt : mts) {
|
||||
JsonNode m = content.get(mt);
|
||||
if (m != null && m.isObject()) {
|
||||
markGwSchema(m.get("schema"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markGwSchema(JsonNode schema) {
|
||||
if (schema == null || !schema.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode s = (ObjectNode) schema;
|
||||
JsonNode props = s.get("properties");
|
||||
if (props != null && props.isObject()) {
|
||||
ArrayNode required = (s.get("required") != null && s.get("required").isArray())
|
||||
? (ArrayNode) s.get("required") : objectMapper.createArrayNode();
|
||||
java.util.Set<String> existing = new java.util.HashSet<>();
|
||||
required.forEach(n -> existing.add(n.asText()));
|
||||
List<String> keys = new ArrayList<>();
|
||||
Iterator<String> it = props.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
keys.add(it.next());
|
||||
}
|
||||
for (String k : keys) {
|
||||
JsonNode p = props.get(k);
|
||||
if (p != null && p.isObject()) {
|
||||
((ObjectNode) p).put("x-djb-gw", true);
|
||||
if (!existing.contains(k)) { // GW 필드는 기본 '필수'
|
||||
required.add(k);
|
||||
existing.add(k);
|
||||
}
|
||||
markGwSchema(p);
|
||||
}
|
||||
}
|
||||
if (required.size() > 0) {
|
||||
s.set("required", required);
|
||||
}
|
||||
}
|
||||
JsonNode items = s.get("items");
|
||||
if (items != null && items.isObject()) {
|
||||
markGwSchema(items);
|
||||
}
|
||||
}
|
||||
|
||||
/** 응답 헤더 미리 등록(중복 시 스킵). */
|
||||
private void addResponseHeader(ObjectNode op, String code, String headerName, String example) {
|
||||
JsonNode resps = op.get("responses");
|
||||
if (resps == null || !resps.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode r = resps.get(code);
|
||||
if (r == null || !r.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode headers = getOrCreateObject((ObjectNode) r, "headers");
|
||||
if (headers.has(headerName)) {
|
||||
return;
|
||||
}
|
||||
ObjectNode h = objectMapper.createObjectNode();
|
||||
h.put("description", headerName);
|
||||
ObjectNode sc = objectMapper.createObjectNode();
|
||||
sc.put("type", "string");
|
||||
h.set("schema", sc);
|
||||
h.put("example", example);
|
||||
headers.set(headerName, h);
|
||||
}
|
||||
|
||||
private JsonNode parseJsonOrText(String s) {
|
||||
try {
|
||||
return objectMapper.readTree(s);
|
||||
} catch (Exception e) {
|
||||
return TextNode.valueOf(s);
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
||||
JsonNode node = parent.get(field);
|
||||
if (node != null && node.isObject()) {
|
||||
return (ObjectNode) node;
|
||||
}
|
||||
ObjectNode created = objectMapper.createObjectNode();
|
||||
parent.set(field, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private void putIfNotNull(Map<String, String> map, String key, String value) {
|
||||
if (value != null) {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutItemUI;
|
||||
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutUI;
|
||||
|
||||
/**
|
||||
* D9 레이아웃 머지: GW LayoutUI 를 마법사 그리드용 필드 목록으로 변환한다.
|
||||
*
|
||||
* <p>GW 에서 온 필드는 {@code sourceType=GW}(항목명/데이터타입 잠금)로 표기하고, 예제값은
|
||||
* {@link DjbApiSpecSampleGenerator} 규칙으로 자동 채운다. 저장된 spec 의 부가정보(설명/예제/제약)
|
||||
* 병합은 클라이언트(그리드)에서 사용자 편집분과 합치므로, 서버는 GW 원본 필드 + 자동 예제까지 제공한다.
|
||||
*/
|
||||
@Service
|
||||
public class DjbApiSpecLayoutMergeService {
|
||||
|
||||
@Autowired
|
||||
private DjbApiSpecSampleGenerator sampleGenerator;
|
||||
|
||||
/** LayoutUI → 그리드 필드 행 목록(flat, depth/parentIndex 로 트리 복원 가능). */
|
||||
public List<Map<String, Object>> toFields(LayoutUI layout, String layoutType) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
if (layout == null || layout.getLayoutItems() == null) {
|
||||
return rows;
|
||||
}
|
||||
for (LayoutItemUI it : layout.getLayoutItems()) {
|
||||
// serno 0 은 root 컨테이너 → 스킵 (ApiSpecManService.generateSpecTableRows 규칙과 동일)
|
||||
if (it.getLoutItemSerno() != null && it.getLoutItemSerno() == 0) {
|
||||
continue;
|
||||
}
|
||||
String dataType = it.getLoutItemDataType();
|
||||
int len = it.getLoutItemLength();
|
||||
int dec = it.getLoutItemDecimal();
|
||||
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("serno", it.getLoutItemSerno());
|
||||
r.put("name", it.getLoutItemName());
|
||||
r.put("desc", it.getLoutItemDesc());
|
||||
r.put("itemType", it.getLoutItemType()); // FIELD / GROUP / GRID
|
||||
r.put("dataType", dataType);
|
||||
r.put("category", sampleGenerator.categoryOf(dataType));
|
||||
r.put("length", len);
|
||||
r.put("decimal", dec);
|
||||
r.put("depth", it.getLoutItemDepth());
|
||||
r.put("parentIndex", it.getParentLoutItemIndex());
|
||||
r.put("required", false);
|
||||
r.put("sourceType", "GW");
|
||||
r.put("example", sampleGenerator.exampleFor(dataType, it.getLoutItemName(), len, dec));
|
||||
rows.add(r);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* D10 예제값 자동 생성 규칙.
|
||||
* <ul>
|
||||
* <li>string → {@code sample_} + 항목명. 최대길이 초과 시 left-cut(앞에서 자름).</li>
|
||||
* <li>integer → {@code 1}</li>
|
||||
* <li>number → {@code 1.1} (소수 자릿수 지정 시 {@code 1.1000} 형태)</li>
|
||||
* <li>boolean → {@code false}</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
public class DjbApiSpecSampleGenerator {
|
||||
|
||||
/** 원시 데이터타입(레이아웃 값)을 OpenAPI 계열 카테고리로 분류. */
|
||||
public String categoryOf(String rawDataType) {
|
||||
if (rawDataType == null) {
|
||||
return "string";
|
||||
}
|
||||
String t = rawDataType.trim().toLowerCase();
|
||||
if (t.contains("bool")) {
|
||||
return "boolean";
|
||||
}
|
||||
if (t.contains("decimal") || t.contains("double") || t.contains("float") || t.contains("number")) {
|
||||
return "number";
|
||||
}
|
||||
if (t.contains("int") || t.contains("long")) {
|
||||
return "integer";
|
||||
}
|
||||
if (t.contains("object") || t.contains("group")) {
|
||||
return "object";
|
||||
}
|
||||
if (t.contains("array") || t.contains("grid") || t.contains("list")) {
|
||||
return "array";
|
||||
}
|
||||
return "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* 예제값 생성.
|
||||
* @param rawDataType 레이아웃 데이터타입(원시)
|
||||
* @param fieldName 항목명
|
||||
* @param maxLen 최대 길이(0/음수면 무제한)
|
||||
* @param decimalLen 소수 자릿수(0이면 미적용)
|
||||
*/
|
||||
public String exampleFor(String rawDataType, String fieldName, int maxLen, int decimalLen) {
|
||||
switch (categoryOf(rawDataType)) {
|
||||
case "integer":
|
||||
return "1";
|
||||
case "number":
|
||||
if (decimalLen > 0) {
|
||||
StringBuilder sb = new StringBuilder("1.");
|
||||
for (int i = 0; i < decimalLen; i++) {
|
||||
sb.append(i == 0 ? '1' : '0');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
return "1.1";
|
||||
case "boolean":
|
||||
return "false";
|
||||
case "object":
|
||||
case "array":
|
||||
return ""; // 컨테이너는 예제값 없음(자식으로 표현)
|
||||
default:
|
||||
String v = "sample_" + StringUtils.defaultString(fieldName);
|
||||
if (maxLen > 0 && v.length() > maxLen) {
|
||||
v = v.substring(0, maxLen); // left-cut
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
/**
|
||||
* 생성/저장된 OpenAPI(3.0) spec JSON 에 인증 체계(components.securitySchemes + 글로벌 security)를 주입한다.
|
||||
*
|
||||
* <p>포털 {@code DjbSwaggerSpecEnricher} 와 동일 모델링:
|
||||
* 게이트웨이가 토큰/키를 표준 Authorization 이 아니라 커스텀 헤더로 받으므로 OAuth·API_KEY 모두
|
||||
* <b>apiKey-in-header</b> 스킴으로 표현(헤더명만 상이).
|
||||
*
|
||||
* <p>authtype(TSEAIHE01.AUTHTYPE, 소문자 저장) 매핑:
|
||||
* {@code oauth/oauth2/ca → OAUTH}, {@code api_key/apikey → API_KEY}, 그 외 → 주입 없음.
|
||||
* 원본 트리를 보존하며 조작한다(문자열 치환 금지). 매핑 불가/빈 스펙/파싱 실패 시 원본 유지(멱등).
|
||||
*/
|
||||
@Service
|
||||
public class DjbApiSpecSwaggerEnricher {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public static final String OAUTH_SCHEME = "djbOAuth";
|
||||
public static final String API_KEY_SCHEME = "djbApiKey";
|
||||
// 게이트웨이 헤더명 (포털 DjbTestbedGatewayProperty 기본값과 동일)
|
||||
public static final String OAUTH_HEADER = "X-AUTH-TOKEN";
|
||||
public static final String API_KEY_HEADER = "X-DJB-API-Key";
|
||||
|
||||
/** authtype → 스킴명. 매핑 불가면 null. */
|
||||
public String schemeNameOf(String authtype) {
|
||||
if (authtype == null) {
|
||||
return null;
|
||||
}
|
||||
switch (authtype.trim().toLowerCase()) {
|
||||
case "oauth":
|
||||
case "oauth2":
|
||||
case "ca":
|
||||
return OAUTH_SCHEME;
|
||||
case "api_key":
|
||||
case "apikey":
|
||||
return API_KEY_SCHEME;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* spec JSON 에 authtype 기반 securityScheme 주입. 매핑 불가/빈 스펙/파싱 실패 시 원본 그대로 반환.
|
||||
*/
|
||||
public String enrich(String specJson, String authtype) {
|
||||
String schemeName = schemeNameOf(authtype);
|
||||
if (StringUtils.isBlank(specJson) || schemeName == null) {
|
||||
return specJson;
|
||||
}
|
||||
try {
|
||||
JsonNode parsed = objectMapper.readTree(specJson);
|
||||
if (!parsed.isObject()) {
|
||||
return specJson;
|
||||
}
|
||||
ObjectNode root = (ObjectNode) parsed;
|
||||
|
||||
ObjectNode scheme = objectMapper.createObjectNode();
|
||||
scheme.put("type", "apiKey");
|
||||
scheme.put("in", "header");
|
||||
scheme.put("name", OAUTH_SCHEME.equals(schemeName) ? OAUTH_HEADER : API_KEY_HEADER);
|
||||
|
||||
boolean swagger2 = root.has("swagger") && !root.has("openapi");
|
||||
if (swagger2) {
|
||||
getOrCreateObject(root, "securityDefinitions").set(schemeName, scheme);
|
||||
} else {
|
||||
ObjectNode components = getOrCreateObject(root, "components");
|
||||
getOrCreateObject(components, "securitySchemes").set(schemeName, scheme);
|
||||
}
|
||||
|
||||
// 글로벌 security 요구사항 (apiKey 스킴은 빈 스코프 배열)
|
||||
ArrayNode security = objectMapper.createArrayNode();
|
||||
ObjectNode requirement = objectMapper.createObjectNode();
|
||||
requirement.set(schemeName, objectMapper.createArrayNode());
|
||||
security.add(requirement);
|
||||
root.set("security", security);
|
||||
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (Exception e) {
|
||||
return specJson; // 파싱/직렬화 실패 → 원본 유지(멱등)
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
||||
JsonNode node = parent.get(field);
|
||||
if (node != null && node.isObject()) {
|
||||
return (ObjectNode) node;
|
||||
}
|
||||
ObjectNode created = objectMapper.createObjectNode();
|
||||
parent.set(field, created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
|
||||
|
||||
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
|
||||
<property name="PREFIX" value="${inst.Type:-ems}" />
|
||||
<property name="LOG_HOME" value="/Users/rinjae/eactive/djb-eapim/djb-eapim/eapim-admin/logs" />
|
||||
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
|
||||
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
|
||||
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="STDOUT"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_stdout.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_stdout.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="ACCESS_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_access.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_access.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="SMS_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/sms.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/sms.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="QUERY_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_query.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_query.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="DYNAMIC_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_dynamic.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_dynamic.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="HIBERNATE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_hibernate.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/${PREFIX}_hibernate.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="HOTSWAP_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_hotswap.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/${PREFIX}_hotswap.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.eactive.eai.rms.onl.server" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.onl.dashboard" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.ext.djb.job.ApiStatusMonitorJob" level="ERROR" />
|
||||
|
||||
<logger name="org.springframework.orm.jpa.JpaTransactionManager" level="ERROR" additivity="false" />
|
||||
|
||||
<logger name="org.springframework" level="INFO" />
|
||||
<logger name="net.sf.ehcache" level="INFO" />
|
||||
|
||||
|
||||
<logger name="ACCESS_LOGGER" level="DEBUG" >
|
||||
<appender-ref ref="ACCESS_APPENDER" />
|
||||
</logger>
|
||||
<logger name="SMS_LOGGER" level="DEBUG" >
|
||||
<appender-ref ref="SMS_APPENDER" />
|
||||
</logger>
|
||||
<logger name="DYNAMIC_LOGGER" level="DEBUG" >
|
||||
<appender-ref ref="DYNAMIC_APPENDER" />
|
||||
</logger>
|
||||
|
||||
|
||||
<!-- QUERY_APPENDER start -->
|
||||
<!-- Hibernate SQL & 파라미터 바인딩 -->
|
||||
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="TRACE" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<!-- Hibernate 일반 로그 (DDL/init/connection 등) -->
|
||||
<logger name="org.hibernate" level="INFO" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
</logger>
|
||||
|
||||
<!-- iBATIS 2.3.4 (commons-logging 또는 log4j-over-slf4j 통해 SLF4J 도달) -->
|
||||
<logger name="com.ibatis" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
</logger>
|
||||
<logger name="com.eactive.eai.rms.common.base.SchemaIdConfiguredSqlMapTemplate" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
</logger>
|
||||
<!-- QUERY_APPENDER end -->
|
||||
|
||||
<!-- HotswapAgent는 자체 logger를 사용하므로 logback에 안 잡힘. hotswap-agent.properties의 LOGFILE로 직접 파일 출력 -->
|
||||
|
||||
<root>
|
||||
<level value="INFO" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user