OpenAPI 스펙 내보내기(UI/DL) 기능 추가

- 내보내기 활성화 여부 설정 키 추가(EXPORT_ENABLED_KEY)
- JSP/JS: 내보내기 버튼 및 다운로드 로직 연동
- YAML/JSON 포맷 저장 기능 구현
This commit is contained in:
Rinjae
2026-07-29 19:24:31 +09:00
parent 2d414dc31b
commit 2422c7f01e
3 changed files with 45 additions and 22 deletions
+24 -13
View File
@@ -349,8 +349,9 @@
}
function fieldRow(label, controlHtml, opts) {
opts = opts || {};
const rowSpacing = opts.bordered ? 'px-3 py-3' : 'mb-3';
return `
<div class="grid grid-cols-12 items-start gap-3 mb-3">
<div class="grid grid-cols-12 items-start gap-3 ${rowSpacing}">
<label class="col-span-3 pt-1.5 text-sm text-slate-600 ${opts.required ? 'font-medium' : ''}">
${escapeHtml(label)}${opts.required ? ' <span class="text-red-500">*</span>' : ''}
${opts.locked ? '<span class="ml-1 text-slate-400 text-xs">🔒</span>' : ''}
@@ -1023,6 +1024,7 @@
root.innerHTML =
card(
sectionTitle('포탈 게시 정보', '개발자포탈 공개 설정. 기존 API 스펙 화면의 값을 불러와 적용합니다.') +
'<div class="border border-slate-200 rounded-lg divide-y divide-slate-200">' +
fieldRow('API 그룹',
`<div class="flex flex-col gap-1">
<div class="flex items-center gap-2 flex-wrap">
@@ -1030,9 +1032,9 @@
<button type="button" id="btn-select-group" class="px-2 py-1 text-sm rounded border border-slate-300 hover:bg-slate-50">+ 선택</button>
</div>
<div class="text-xs text-slate-400">API 그룹은 apiGroupMan(API 그룹 관리)에서도 관리됩니다.</div>
</div>`) +
fieldRow('공개 여부', `<label class="inline-flex items-center gap-2 text-sm">${checkbox(p.displayYn === 'Y', 'data-portal-yn')} 공개</label>`) +
fieldRow('공개 한',
</div>`, { bordered: true }) +
fieldRow('포탈 게시 활성화', `<label class="inline-flex items-center gap-2 text-sm">${checkbox(p.displayYn === 'Y', 'data-portal-yn')} 활성화</label>`, { bordered: true }) +
fieldRow('역할별 공개 한',
`<div class="flex flex-col gap-1">
<div class="flex gap-4">
<label class="inline-flex items-center gap-1 text-sm">${checkbox(!!roles.ROLE_USER, 'data-role="ROLE_USER"')} 개인사용자</label>
@@ -1040,7 +1042,7 @@
<label class="inline-flex items-center gap-1 text-sm">${checkbox(!!roles.ROLE_CORP_MANAGER, 'data-role="ROLE_CORP_MANAGER"')} 법인관리자</label>
</div>
<div class="text-xs text-slate-400">전부 선택 시 로그인 사용자 전체에게 공개됩니다.</div>
</div>`) +
</div>`, { bordered: true }) +
(showOrg ? fieldRow('공개 법인',
`<div class="flex flex-col gap-1">
<div class="flex items-center gap-2 flex-wrap">
@@ -1048,7 +1050,7 @@
<button type="button" id="btn-select-org" class="px-2 py-1 text-sm rounded border border-slate-300 hover:bg-slate-50">+ 선택</button>
</div>
<div class="text-xs text-slate-400">특정 법인에게만 공개하고 싶을 때 공개 법인을 선택하시면 됩니다.</div>
</div>`) : '') +
</div>`, { bordered: true }) : '') +
fieldRow('응답 유형',
'<div>' +
['sample', 'mock', 'gw'].map(v =>
@@ -1060,20 +1062,23 @@
+ `<div class="mt-1 text-xs text-slate-500">호출 방식: <b>${useProxyOn() ? '포탈 프록시 경유' : '직접 호출'}</b>`
+ (useProxyOn() ? ' (<code class="font-mono">/api/call-api</code> 경유, 인증 헤더 자동 주입)' : ' (브라우저 → 대상 직접 호출, 대상 CORS 허용 필요)') + `</div>`) +
(rt === 'gw' ? `<div class="mt-1 text-xs text-slate-400">GW 주소는 포탈 설정(PTL_PROPERTY, 그룹 Portal)의 <code class="font-mono">djb.gateway.base-url</code> 값을 사용합니다.</div>` : '') +
'</div>') +
(rt === 'mock' ? fieldRow('Mock Server URL', input(p.mockUrl || '', 'data-portal="mockUrl"', { placeholder: 'https://mock.example.com/api/v1/foo' }), { hint: 'Mock 응답을 제공할 전체 호출 URL(경로 포함). 응답 유형이 Mock 일 때 이 주소로 그대로 호출하며, 어댑터/오퍼레이션 경로를 뒤에 덧붙이지 않습니다.' }) : '')
'</div>', { bordered: true }) +
(rt === 'mock' ? fieldRow('Mock Server URL', input(p.mockUrl || '', 'data-portal="mockUrl"', { placeholder: 'https://mock.example.com/api/v1/foo' }), { hint: 'Mock 응답을 제공할 전체 호출 URL(경로 포함). 응답 유형이 Mock 일 때 이 주소로 그대로 호출하며, 어댑터/오퍼레이션 경로를 뒤에 덧붙이지 않습니다.', bordered: true }) : '') +
'</div>'
) +
card(
sectionTitle('설명 (API 소개 페이지)', 'API 소개 페이지에만 표시됩니다. OpenAPI 스펙 파일에는 포함되지 않습니다.') +
'<div id="info-desc-editor">' + (state.data.info.description.value || '') + '</div>'
) +
// 내보내기 카드는 PTL_PROPERTY(djb.openapi.export.enabled=true) 일 때만 노출. 기본 미노출.
((window.DJB_CTX && window.DJB_CTX.exportEnabled) ?
card(
sectionTitle('내보내기', 'OpenAPI 스펙을 파일로 저장합니다.') +
`<div class="flex gap-2">
<button data-export="yaml" class="px-4 py-2 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">YAML 다운로드</button>
<button data-export="json" class="px-4 py-2 text-sm rounded border border-brand-300 text-brand-700 hover:bg-brand-50">JSON 다운로드</button>
</div>`
);
) : '');
initInfoDescEditor(); // 설명 리치 에디터 (API 소개 페이지 전용)
}
@@ -1653,7 +1658,7 @@
$('#btn-prev').addEventListener('click', () => goStep(state.step - 1));
$('#btn-next').addEventListener('click', () => {
if (state.step < STEPS.length) goStep(state.step + 1);
else toast('모든 단계를 완료했습니다. 우측 미리보기 또는 내보내기를 사용하세요.');
else saveSpec(false); // 마지막 단계의 "완료" → 저장 동작
});
$('#btn-toggle-preview').addEventListener('click', () => setPreviewVisibility(false));
@@ -1686,7 +1691,10 @@
toast('변경을 취소했습니다');
});
$('#btn-export').addEventListener('click', () => $('#export-menu').classList.toggle('hidden'));
// 내보내기 버튼은 djb.openapi.export.enabled=true 일 때만 DOM 에 존재 → 없으면 바인딩 생략
var bExport = $('#btn-export');
if (bExport) {
bExport.addEventListener('click', () => $('#export-menu').classList.toggle('hidden'));
document.addEventListener('click', e => {
if (!e.target.closest('#btn-export') && !e.target.closest('#export-menu')) $('#export-menu').classList.add('hidden');
});
@@ -1694,6 +1702,7 @@
const b = e.target.closest('[data-export]');
if (b) { downloadSpec(b.dataset.export); $('#export-menu').classList.add('hidden'); }
});
}
var bSave = $('#btn-save'); if (bSave) bSave.addEventListener('click', () => saveSpec(false));
var bRegen = $('#btn-regen'); if (bRegen) bRegen.addEventListener('click', regenerate);
@@ -2000,8 +2009,10 @@
state.data.msgSpec = state.data.msgSpec || { request: '', response: '' };
if (res.apiRequestSpec) state.data.msgSpec.request = decodeEntities(res.apiRequestSpec);
if (res.apiResponseSpec) state.data.msgSpec.response = decodeEntities(res.apiResponseSpec);
// 저장본이 아니면(자동생성/최초 성) 추가자료(예제·설명자료)를 프론트에서 스키마 기준 생성
if ((res.source || '') !== 'saved') frontAutoGenerate();
// 저장본이 아니면(최초 성) 자동생성 여부를 confirm 으로 확인 후 진행
if ((res.source || '') !== 'saved') {
if (window.confirm('저장된 스펙이 없습니다. 게이트웨이 레이아웃 기반으로 예제·설명자료를 자동 생성할까요?')) frontAutoGenerate();
}
})
.catch(e => console.error('loadInitialData', e));
}
@@ -69,6 +69,7 @@
gwAddress: "${gwAddress}",
useProxy: ${useProxy},
tokenUseProxy: ${tokenUseProxy},
exportEnabled: ${exportEnabled},
orgPopupUrl: "<c:url value='/onl/transaction/apim/apiSpecMan.view'/>",
groupPopupUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.view'/>?cmd=GROUP_POPUP",
jsonUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>"
@@ -93,6 +94,8 @@
<button id="btn-newwin" class="hidden px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">새창으로 띄우기</button>
<button id="btn-regen" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">자동 생성(초기화)</button>
<button id="btn-save" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700">저장</button>
<%-- 내보내기 버튼은 PTL_PROPERTY(djb.openapi.export.enabled=true) 일 때만 노출. 기본 미노출. --%>
<c:if test="${exportEnabled}">
<div class="relative">
<button id="btn-export" class="px-3 py-1.5 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">내보내기 ▾</button>
<div id="export-menu" class="hidden absolute right-0 mt-1 w-44 bg-white border border-slate-200 rounded shadow-lg z-40">
@@ -100,6 +103,7 @@
<button data-export="json" class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-50">JSON 다운로드</button>
</div>
</div>
</c:if>
<button id="btn-close" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">닫기</button>
</div>
</header>
@@ -75,6 +75,8 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
// 테스트베드 프록시 사용 여부(포탈 DjbTestbedGatewayProperty 와 동일 키). 마법사에 호출/인증 프록시 경유 안내용.
private static final String USE_PROXY_KEY = "djb.gateway.use-proxy";
private static final String TOKEN_USE_PROXY_KEY = "djb.gateway.token-use-proxy";
// OpenAPI 스펙 내보내기(YAML/JSON 다운로드) UI 노출 여부. 기본 false(사용안함).
private static final String EXPORT_ENABLED_KEY = "djb.openapi.export.enabled";
// "PortalMock" 은 포탈이 요청 origin 으로 치환하는 sentinel 이라 admin 미리보기용 서버 주소로는 부적합 → 대체값 사용.
private static final String GW_PREVIEW_FALLBACK = "http://127.0.0.1:39310";
@@ -91,14 +93,20 @@ public class DjbApiSpecController extends OnlBaseAnnotationController {
// 테스트베드 프록시(/api/call-api) 사용 여부 — mock/gw 호출·인증 프록시 경유 안내 표기용
model.addAttribute("useProxy", resolveBoolProperty(USE_PROXY_KEY, true));
model.addAttribute("tokenUseProxy", resolveBoolProperty(TOKEN_USE_PROXY_KEY, true));
// 내보내기 UI 노출 여부(djb.openapi.export.enabled) — 미설정 시 false(사용안함)로 생성
model.addAttribute("exportEnabled", resolveBoolProperty(
EXPORT_ENABLED_KEY, false, "OpenAPI 스펙 내보내기(YAML/JSON) UI 노출 여부(true/false)"));
return "/onl/transaction/apim/djbApiSpecManPopup";
}
/** 포탈 그룹 프로퍼티(불리언, 레거시 Y/N 포함)를 읽어 boolean 으로 해석. 미설정/파싱불가 시 default. */
private boolean resolveBoolProperty(String key, boolean def) {
return resolveBoolProperty(key, def, "테스트베드 프록시(/api/call-api) 사용 여부(true/false)");
}
private boolean resolveBoolProperty(String key, boolean def, String description) {
String v = portalPropertyService.getOrCreateProperty(
"Portal", key, def ? "true" : "false",
"테스트베드 프록시(/api/call-api) 사용 여부(true/false)");
"Portal", key, def ? "true" : "false", description);
if (v == null) {
return def;
}