API 그룹 팝업 JSP 추가 - Swagger Validator JS 등록 - Summernote CSS 추가

- API 그룹 팝업(djbApiGroupPopup.jsp) 화면 파일 추가
- Swagger Validator (djb-swagger-validator.js) 및 관련 리팩토링
- Summernote 콘텐츠 스타일 정의 (editor-content.css) 추가
This commit is contained in:
Rinjae
2026-07-03 16:54:47 +09:00
parent 6c0fca22f3
commit b7c8e7f41c
14 changed files with 1242 additions and 381 deletions
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;
}
}
@@ -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;
@@ -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>
@@ -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>
@@ -10,7 +10,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=1600" />
<title>DJB API 문서 에디터 — OpenAPI Editor</title>
<title>OpenAPI Spec 에디터</title>
<%-- Tailwind (벤더된 Play CDN JS, 오프라인) --%>
<script src="<c:url value='/plugins/openapi/tailwind.js'/>"></script>
@@ -33,9 +33,12 @@
<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>
@@ -64,7 +67,8 @@
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/apiSpecManPopup.view'/>",
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>
@@ -103,7 +107,7 @@
<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-6 px-2"></ol>
<ol id="stepper" class="grid grid-cols-5 px-2"></ol>
</nav>
<!-- ===== Body ===== -->
@@ -111,7 +115,7 @@
<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> / 6</span>
<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>
+80 -85
View File
@@ -1,10 +1,9 @@
plugins {
id 'java-library'
id 'java-library'
id 'eclipse'
id 'eclipse-wtp'
id 'eclipse-wtp'
id 'idea'
id 'war'
id 'com.diffplug.eclipse.apt' version '3.41.1'
}
group 'com.eactive'
@@ -32,11 +31,11 @@ allprojects {
project.webAppDirName = 'WebContent'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
}
sourceSets {
main {
java {
@@ -46,56 +45,56 @@ sourceSets {
}
compileJava {
options.encoding = 'UTF-8'
options.compilerArgs = ['-parameters']
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
aptOptions {
processorArgs = [ 'querydsl.generatedAnnotationClass' : 'com.querydsl.core.annotations.Generated' ]
}
options.encoding = 'UTF-8'
options.compilerArgs = ['-parameters']
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
}
war {
def profile = project.findProperty("profile") ?: "tomcat"
if (profile == "weblogic") {
webXml = file("WebContent/WEB-INF/weblogic-web.xml")
exclude 'WEB-INF/web.xml'
}
// rootSpec.exclude '**/persistence.xml'
exclude '**/context.xml'
exclude '**/context-*.xml'
archiveFileName = "eapim-admin.war"
duplicatesStrategy = DuplicatesStrategy.WARN
def profile = project.findProperty("profile") ?: "tomcat"
if (profile == "weblogic") {
webXml = file("WebContent/WEB-INF/weblogic-web.xml")
exclude 'WEB-INF/web.xml'
}
// rootSpec.exclude '**/persistence.xml'
exclude '**/context.xml'
exclude '**/context-*.xml'
archiveFileName = "eapim-admin.war"
duplicatesStrategy = DuplicatesStrategy.WARN
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
options.compilerArgs += ['-parameters', "-Xlint:unchecked", "-Xlint:deprecation"]
options.encoding = 'UTF-8'
options.compilerArgs += ['-parameters', "-Xlint:unchecked", "-Xlint:deprecation"]
}
dependencies {
annotationProcessor "org.projectlombok:lombok:1.18.28", "org.projectlombok:lombok-mapstruct-binding:0.2.0", "org.mapstruct:mapstruct-processor:1.5.5.Final"
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jpa", "jakarta.persistence:jakarta.persistence-api:2.2.3", "jakarta.annotation:jakarta.annotation-api:1.3.5"
annotationProcessor "org.projectlombok:lombok:1.18.28",
"org.projectlombok:lombok-mapstruct-binding:0.2.0",
"org.mapstruct:mapstruct-processor:1.5.5.Final"
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jpa",
"jakarta.persistence:jakarta.persistence-api:2.2.3",
"jakarta.annotation:jakarta.annotation-api:1.3.5"
implementation project(':elink-online-core')
implementation project(':elink-online-transformer')
implementation project(':elink-online-common')
implementation project(':elink-online-emsclient')
implementation project(':elink-portal-common')
//implementation project(':kjb-safedb')
implementation project(':elink-online-emsclient')
implementation project(':elink-portal-common')
//implementation project(':kjb-safedb')
implementation project(":eapim-admin-djb")
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
compileOnly files('libs/damo-manager.jar')
//implementation "org.dom4j:dom4j:2.1.3"
//implementation "org.dom4j:dom4j:2.1.3"
//implementation "com.rabbitmq:amqp-client:3.6.6"
implementation "commons-primitives:commons-primitives:1.0"
@@ -117,10 +116,10 @@ dependencies {
// https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1'
//implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
implementation "javax.jms:javax.jms-api:2.0"
implementation "org.snmp4j:snmp4j:1.10.1"
@@ -187,32 +186,32 @@ dependencies {
implementation 'ch.qos.logback:logback-classic:1.2.10'
implementation 'ch.qos.logback:logback-access:1.2.10'
implementation 'ch.qos.logback:logback-core:1.2.10'
implementation files("libs/eai-bap-client.jar")
compileOnly 'javax.xml.bind:jaxb-api:2.3.1'
//implementation 'org.postgresql:postgresql:42.2.23'
implementation 'backport-util-concurrent:backport-util-concurrent:3.0'
implementation ('software.amazon.awssdk:s3:2.20.142') {
exclude group: 'org.slf4j', module: 'slf4j-api'
exclude group: 'org.slf4j', module: 'logback-classic'
}
implementation 'software.amazon.awssdk:sso:2.20.142'
implementation 'software.amazon.awssdk:sts:2.20.142'
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
implementation files("libs/eai-bap-client.jar")
// JDK 8 rt.jar org.w3c.dom.ElementTraversal xercesImpl NCDFE .
// xml-apis 1.4.01 . WAR .
implementation 'xml-apis:xml-apis:1.4.01'
compileOnly 'javax.xml.bind:jaxb-api:2.3.1'
testRuntimeOnly 'com.h2database:h2:2.1.214'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15'
//implementation 'org.postgresql:postgresql:42.2.23'
implementation 'backport-util-concurrent:backport-util-concurrent:3.0'
implementation ('software.amazon.awssdk:s3:2.20.142') {
exclude group: 'org.slf4j', module: 'slf4j-api'
exclude group: 'org.slf4j', module: 'logback-classic'
}
implementation 'software.amazon.awssdk:sso:2.20.142'
implementation 'software.amazon.awssdk:sts:2.20.142'
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
// JDK 8 rt.jar org.w3c.dom.ElementTraversal xercesImpl NCDFE .
// xml-apis 1.4.01 . WAR .
implementation 'xml-apis:xml-apis:1.4.01'
testRuntimeOnly 'com.h2database:h2:2.1.214'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15'
}
test {
@@ -220,9 +219,9 @@ test {
}
configurations.all {
exclude group: 'log4j', module: 'log4j'
exclude group: 'org.apache.activemq'
exclude group: 'org.codehaus.jackson'
exclude group: 'log4j', module: 'log4j'
exclude group: 'org.apache.activemq'
exclude group: 'org.codehaus.jackson'
resolutionStrategy {
// 10 minute cache of dynamic version navigation
cacheDynamicVersionsFor 10, 'minutes'
@@ -246,29 +245,25 @@ task settingEclipseEncoding {
}
task initDirs() {
file(generatedJavaDir).mkdirs()
file(generatedJavaDir).mkdirs()
}
eclipse {
wtp {
component {
contextPath = 'monitoring'
}
}
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)
}
}
project {
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
natures.add('org.eclipse.buildship.core.gradleprojectnature')
buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
}
}
wtp {
component {
contextPath = 'monitoring'
}
}
synchronizationTasks settingEclipseEncoding, initDirs
jdt {
}
project {
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
natures.add('org.eclipse.buildship.core.gradleprojectnature')
buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
}
}
}
tasks.withType(JavaCompile) {
+2 -2
View File
@@ -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,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);
}
}
}
}
@@ -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);
@@ -15,4 +15,7 @@ public class PropertyUI {
@JsonProperty("PRPTY2VAL")
private String prpty2Val;
@JsonProperty("PRPTYDESC")
private String prptyDesc;
}
@@ -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";
}
@@ -29,6 +29,8 @@ 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;
/**
@@ -61,19 +63,37 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
@Autowired
private PortalPropertyService portalPropertyService;
@Autowired
private ApiGroupService apiGroupService;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final String SWAGGER_GW_ADDRESS_DEFAULT = "http://127.0.0.1:39310";
// 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 주소(swagger.gw.address, PTL_PROPERTY 그룹 'Portal') 없으면 기본값으로 생성
model.addAttribute("gwAddress",
portalPropertyService.getOrCreateProperty("Portal", "swagger.gw.address", SWAGGER_GW_ADDRESS_DEFAULT, "Swagger GW 주소"));
// 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
@@ -106,6 +126,19 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
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());
@@ -124,13 +157,15 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
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) {
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");
@@ -153,6 +188,21 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
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);
@@ -272,7 +322,15 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
tags.add(tag);
root.set("tags", tags);
// servers 유지(상대 path). 최종 서버 base 프론트에서 응답유형(gw/mock/기본) 따라 선택.
// 어댑터경로(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;
@@ -298,13 +356,18 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
ArrayNode opTags = objectMapper.createArrayNode();
opTags.add(tagName);
op.set("tags", opTags);
setContentExample(op, reqSample);
setResponseExample(op, "200", resSample);
// 예제(요청/응답 본문) 프론트에서 스키마 기준으로 생성 자동생성/재생성 결과 일치. 여기선 임베드하지 않음.
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);