diff --git a/WebContent/js/djb/apispec/app.js b/WebContent/js/djb/apispec/app.js
index 28dbd77..bfe9476 100644
--- a/WebContent/js/djb/apispec/app.js
+++ b/WebContent/js/djb/apispec/app.js
@@ -8,12 +8,11 @@
// 1. 상태
// ============================================================
const STEPS = [
- { id: 1, title: '기본정보', hint: 'API 의 메타 정보(설명, 연락처, 라이선스, 태그) 를 입력합니다.' },
- { id: 2, title: '엔드포인트', hint: '환경별 서버 URL 과 오퍼레이션 메타정보를 작성합니다.' },
- { id: 3, title: '파라미터/스키마', hint: '요청 파라미터, 본문, 응답 스키마를 정의합니다.' },
- { id: 4, title: '인증/보안', hint: 'API 인증 방식과 적용 범위를 설정합니다.' },
- { id: 5, title: '예제/응답', hint: '실제 요청/응답 예제와 헤더를 입력합니다.' },
- { id: 6, title: '포탈 게시 정보', hint: '개발자포탈 공개 설정(공개 여부/권한/법인/응답유형)을 지정합니다.' }
+ { id: 1, title: '기본정보', hint: 'API 메타 정보·엔드포인트·인증/보안을 확인·입력합니다.' },
+ { id: 2, title: '요청 정보', hint: '요청 Header/Parameter/Body 와 API 설명(HTML)을 작성합니다.' },
+ { id: 3, title: '응답 정보', hint: '응답 Header/Body 와 API 설명(HTML)을 작성합니다.' },
+ { id: 4, title: '요청/응답 샘플', hint: '요청/응답 예제를 입력합니다.' },
+ { id: 5, title: '포탈 게시 정보', hint: '개발자포탈 공개 설정(공개 여부/권한/법인/응답유형)을 지정합니다.' }
];
const state = {
@@ -107,6 +106,7 @@
// ============================================================
function buildOpenApiSpec() {
const d = state.data;
+ syncResponseExamplesFromSchema(); // 응답 예제 = 응답 정보(스키마·예제값) 파생 — 저장/미리보기 일관
const spec = {
openapi: (d.__openapi || '3.1.0'),
info: {
@@ -187,13 +187,16 @@
}
});
- // 서버 base = 응답유형별 단일 서버 (gw=swagger.gw.address, mock=MockURL, 그 외=레이아웃 어댑터 주소)
+ // 경로(op.path.value)에는 어댑터경로가 이미 baking 됨(백엔드 applyAutoRules). 서버에는 응답유형별 호스트만 붙인다.
+ // - gw : djb.gateway.base-url (PortalProperty) → [gw호스트 + 어댑터경로 + 경로]
+ // - mock : Mock Server URL → [mock호스트 + 어댑터경로 + 경로]
+ // - 그 외 : 호스트 없음 → [어댑터경로 + 경로]
var _rt = (d.portal && d.portal.responseType) || 'sample';
- var _base;
- if (_rt === 'gw') _base = (window.DJB_CTX && window.DJB_CTX.gwAddress) || 'http://127.0.0.1:39310';
- else if (_rt === 'mock') _base = (d.portal && d.portal.mockUrl) || '';
- else _base = (d.servers && d.servers[0] && d.servers[0].url && d.servers[0].url.value) || '';
- if (_base) spec.servers = [{ url: _base }];
+ var _host;
+ if (_rt === 'gw') _host = (window.DJB_CTX && window.DJB_CTX.gwAddress) || 'http://127.0.0.1:39310';
+ else if (_rt === 'mock') _host = (d.portal && d.portal.mockUrl) || '';
+ else _host = '';
+ if (_host) spec.servers = [{ url: _host }];
spec.paths[op.path.value] = { [op.method.value.toLowerCase()]: operation };
// 보안 스킴
@@ -298,10 +301,12 @@
function goStep(n) {
if (n < 1 || n > STEPS.length) return;
state.step = n;
+ if (n === 2) state.reqInfoTab = 'body'; // 요청/응답 정보: 진입 시 항상 Body 먼저
+ if (n === 3) state.resInfoTab = 'body';
renderStepper();
renderForm();
- if (state.step === 3) {
- setPreviewVisibility(false); // 파라미터/스키마: 우측 미리보기 패널 숨김
+ if (state.step === 2 || state.step === 3) {
+ setPreviewVisibility(false); // 요청/응답 정보(넓은 테이블): 우측 미리보기 패널 숨김
} else {
setPreviewVisibility(true);
restoreNormalPreviewLayout();
@@ -315,12 +320,11 @@
const root = $('#form-content');
root.innerHTML = '';
switch (state.step) {
- case 1: renderStep1(root); break;
- case 2: renderStep2(root); break;
- case 3: renderStep3(root); break;
- case 4: renderStep4(root); break;
- case 5: renderStep5(root); break;
- case 6: renderStep6(root); break;
+ case 1: renderStep1(root); break; // 기본정보(+엔드포인트+인증/보안)
+ case 2: renderReqInfo(root); break; // 요청 정보 (Header/Parameter/Body/API 설명)
+ case 3: renderResInfo(root); break; // 응답 정보 (Header/Body/API 설명)
+ case 4: renderStep5(root); break; // 요청/응답 샘플
+ case 5: renderStep6(root); break; // 포탈 게시 정보
}
}
@@ -349,13 +353,26 @@
// ============================================================
function renderStep1(root) {
const i = state.data.info;
+ const op = state.data.operation;
+ const methodColor = {
+ GET: 'bg-emerald-100 text-emerald-700 border-emerald-300',
+ POST: 'bg-blue-100 text-blue-700 border-blue-300',
+ PUT: 'bg-amber-100 text-amber-700 border-amber-300',
+ DELETE: 'bg-red-100 text-red-700 border-red-300'
+ }[op.method.value] || 'bg-slate-100 text-slate-700 border-slate-300';
root.innerHTML = `
${card(
sectionTitle('API 정보', '게이트웨이가 제공한 식별 정보와 사용자가 작성하는 설명을 분리해 표시합니다.') +
+ `
Swagger + API 소개 페이지에 사용됨. Swagger 에서 일부 태그가 제거될 수 있음.
')
+ fieldRow('상세 설명', textarea(op.description.value, 'data-path="operation.description.value"', { rows: 4, placeholder: '동작, 비즈니스 규칙, 주의사항' }) + '
${state.data.tags.map(t => `
@@ -435,57 +432,147 @@
// ============================================================
// 8. Step 3 — 파라미터 / 스키마 (핵심)
// ============================================================
- function renderStep3(root) {
- // Request Body 만 접근 (파라미터 / Response / Components 미노출)
- state.step3Section = 'body';
- root.innerHTML = renderBodyTable();
- }
- function renderStep3Section() {
- // Step3 = Request Body 만. 컨테이너는 #form-content(직접). (add-row/자식추가/토글 재렌더용)
- const c = $('#form-content');
- if (!c) return;
- c.innerHTML = renderBodyTable();
+ // 서브 탭 바 (요청/응답 정보 공용)
+ function subTabBar(tabs, cur, attr) {
+ return '
'
+ + tabs.map(t => ``).join('')
+ + '
';
}
- // --- Parameters 테이블 ---
- function renderParamsTable() {
- const rows = state.data.parameters.map((p, i) => `
+ // Step 2 — 요청 정보 (Header / Parameter / Body / API 설명(HTML))
+ function renderReqInfo(root) {
+ const tab = state.reqInfoTab || 'body';
+ const tabs = [
+ { key: 'header', label: 'Header' },
+ { key: 'param', label: 'Parameter' },
+ { key: 'body', label: 'Body' },
+ { key: 'desc', label: 'API 설명(HTML)' }
+ ];
+ let body;
+ if (tab === 'header') body = renderParamsTable('header');
+ else if (tab === 'param') body = renderParamsTable('param');
+ else if (tab === 'body') body = renderBodyTable();
+ else body = card(
+ sectionTitle('요청 API 설명(HTML)', 'API 소개(설명) 페이지에 표시되는 자료입니다.') +
+ `
'
+ );
+ root.innerHTML = subTabBar(tabs, tab, 'data-reqinfo-tab') + body;
+ if (tab === 'desc') {
+ var fc = document.getElementById('form-content');
+ // 카드/탭바/툴바/여백 오버헤드를 제외해 폼 영역에 세로스크롤이 생기지 않는 높이로
+ var h = Math.max(300, (fc ? fc.clientHeight : 600) - 290);
+ initSpecEditor('#msg-editor', 'request', h);
+ }
+ }
+
+ // Step 3 — 응답 정보 (Header / Body / API 설명(HTML))
+ function renderResInfo(root) {
+ const tab = state.resInfoTab || 'body';
+ const tabs = [
+ { key: 'header', label: 'Header' },
+ { key: 'body', label: 'Body' },
+ { key: 'desc', label: 'API 설명(HTML)' }
+ ];
+ let body;
+ if (tab === 'header') body = renderResponseHeaderCard();
+ else if (tab === 'body') body = renderResponseTable();
+ else body = card(
+ sectionTitle('응답 API 설명(HTML)', 'API 소개(설명) 페이지에 표시되는 자료입니다.') +
+ `
'
+ );
+ root.innerHTML = subTabBar(tabs, tab, 'data-resinfo-tab') + body;
+ if (tab === 'desc') {
+ var fc = document.getElementById('form-content');
+ // 카드/탭바/툴바/여백 오버헤드를 제외해 폼 영역에 세로스크롤이 생기지 않는 높이로
+ var h = Math.max(300, (fc ? fc.clientHeight : 600) - 290);
+ initSpecEditor('#msg-editor', 'response', h);
+ }
+ }
+
+ // 응답 Header 카드 — 상태코드별 응답 헤더(examples.response[code].headers)
+ function renderResponseHeaderCard() {
+ const codes = Object.keys(state.data.examples.response);
+ const cur = state.responseStatusTab;
+ const curSet = state.data.examples.response[cur] || { body: '', headers: [] };
+ return card(
+ sectionTitle('응답 Header', '상태코드별 응답 헤더를 정의합니다. 추가한 상태코드는 Body/[요청/응답 샘플]과 연동됩니다.') +
+ `
+ ${codes.map(code => `
+
+ `).join('')}
+
+
` +
+ `
+
+ | name | type | description | example | |
+
+
+ ${(curSet.headers || []).map((h, i) => `
+
+ | ${input(h.name, `data-resp-header="${i}.name"`)} |
+ ${selectInput(h.type, ['string', 'integer', 'number', 'boolean'], `data-resp-header="${i}.type"`)} |
+ ${input(h.description, `data-resp-header="${i}.description"`)} |
+ ${input(h.example, `data-resp-header="${i}.example"`)} |
+ |
+
+ `).join('')}
+
+
+
+
+
`
+ );
+ }
+
+ function renderStep3Section() {
+ // add-row/자식추가/토글/탭전환 재렌더용 — 현재 스텝 폼 재렌더(활성 서브탭 유지)
+ renderForm();
+ }
+
+ // --- Parameters 테이블 (kind: 'header' = in=header / 'param' = query 전용) ---
+ function renderParamsTable(kind) {
+ const isHeader = kind === 'header';
+ const entries = state.data.parameters
+ .map((p, i) => ({ p: p, i: i }))
+ .filter(e => isHeader ? e.p.in === 'header' : e.p.in === 'query');
+ const rows = entries.map(e => {
+ const p = e.p, i = e.i;
+ return `
- | ${selectInput(p.in, ['header', 'query', 'path', 'cookie'], `data-pparam-in="${i}"`)} |
${p.name.locked ? lockedInput(p.name.value) : input(p.name.value, `data-path="parameters.${i}.name.value"`)} |
+ ${input(p.description.value, `data-path="parameters.${i}.description.value"`, { placeholder: '설명' })} |
${p.type.locked
? lockedInput(p.type.value)
: selectInput(p.type.value, ['string', 'integer', 'number', 'boolean'], `data-path="parameters.${i}.type.value"`)} |
+ ${input(p.maxLength.value, `data-path="parameters.${i}.maxLength.value"`, { placeholder: '0', extra: 'text-xs' })} |
${checkbox(p.required.value, `data-path="parameters.${i}.required.value" data-type="bool"`, { locked: p.required.locked })} |
- ${selectInput(p.format.value, ['', 'uuid', 'date', 'date-time', 'email', 'uri'], `data-path="parameters.${i}.format.value"`)} |
- ${input(p.maxLength.value, `data-path="parameters.${i}.maxLength.value"`, { placeholder: 'maxLength', extra: 'text-xs' })} |
- ${input(p.description.value, `data-path="parameters.${i}.description.value"`, { placeholder: '설명' })} |
- ${input(p.example.value, `data-path="parameters.${i}.example.value"`, { placeholder: '예제' })} |
+ ${input(p.example.value, `data-path="parameters.${i}.example.value"`, { placeholder: '예제값' })} |
- ${p.name.locked ? '🔒' : ``}
+ ${p.name.locked ? '' : ``}
|
-
- `).join('');
+ `;
+ }).join('');
return card(
- sectionTitle('파라미터 (Header / Query / Path / Cookie)', '게이트웨이가 등록한 파라미터는 잠금됩니다. 추가 파라미터는 자유롭게 정의하세요.') +
+ sectionTitle(isHeader ? '요청 Header' : 'Parameter (Query)', '게이트웨이가 등록한 항목은 잠금됩니다. 추가 항목은 자유롭게 정의하세요.') +
`
- | 위치 |
이름 |
- 타입 |
- 필수 |
- format |
- maxLen |
설명 |
- 예제 |
+ 타입 |
+ 길이 |
+ 필수 |
+ 예제값 |
|
- ${rows}
+ ${rows || `| 등록된 항목이 없습니다 |
`}
-
+
`
);
}
@@ -494,10 +581,12 @@
function renderBodyTable() {
return card(
sectionTitle('Request Body 스키마', `media type: ${state.data.requestBody.mediaType}. 중첩된 object/array 를 모두 지원합니다.`) +
- `
-
- ${checkbox(state.data.requestBody.required.value, 'data-path="requestBody.required.value" data-type="bool"', { locked: state.data.requestBody.required.locked })}
-
🔒
+ `
+
기본 동작과 잠금
+
+ - 여기서 설정하는 validation(타입·필수·제약 등)은 실제 게이트웨이(GW) 동작에는 영향을 주지 않습니다. 개발자 포털과 OpenAPI Spec 문서에만 반영되는 참고 정보입니다.
+ - 🔒 표시된 필드는 GW 레이아웃에 등록된 필드로, 삭제하거나 속성(이름·타입·필수 등)을 변경할 수 없습니다. 추가 필드는 자유롭게 정의·삭제할 수 있습니다.
+
` +
renderSchemaTable(state.data.requestBody.schema, 'requestBody.schema') +
`
@@ -514,12 +603,14 @@
const tabsHtml = codes.map(code => `
`).join('');
return card(
- sectionTitle('Response 스키마', '상태 코드별로 응답 스키마를 정의합니다.') +
- `
${tabsHtml}
` +
+ sectionTitle('Response 스키마', '상태 코드별로 응답 스키마를 정의합니다. 추가한 상태코드는 [요청/응답 샘플]의 응답 예제와 연동됩니다.') +
+ `
${tabsHtml}
+
+
` +
`
${fieldRow('설명', input(r.description.value, `data-path="responses.${cur}.description.value"`, { placeholder: '응답 설명' }))}
` +
renderSchemaTable(r.schema, `responses.${cur}.schema`) +
`
@@ -543,38 +634,38 @@
- | 필드명 |
- 타입 |
- 필수 |
- format |
- maxLen |
- 제약 |
+ # |
+ 잠금 |
+ 필드명 |
설명 |
- 예제 |
+ 타입 |
+ 길이 |
+ 필수 |
+ 예제값 |
|
- ${renderSchemaRows(rows, basePath, 0)}
+ ${renderSchemaRows(rows, basePath, 0, '')}
`;
}
- function renderSchemaRows(rows, basePath, depth) {
+ function renderSchemaRows(rows, basePath, depth, seqPrefix) {
let html = '';
rows.forEach((row, idx) => {
const path = `${basePath}.${idx}`;
+ const seq = seqPrefix ? `${seqPrefix}-${idx + 1}` : String(idx + 1); // 계층 시퀀스 (1, 1-1, 2-1-1 …)
const isLocked = row.name.locked;
const isObject = row.type.value === 'object';
const isArray = row.type.value === 'array';
- const hasChildren = isObject && row.children && row.children.length;
const expandedKey = `${path}.__expanded`;
const isExpanded = state[expandedKey] !== false;
- const constraintsKey = `${path}.__constraints`;
- const showConstraints = !!state[constraintsKey];
html += `
+ | ${seq} |
+ ${isLocked ? '🔒' : ''} |
${isObject || isArray
@@ -582,40 +673,27 @@
: ` `}
${isObject ? '{}' : isArray ? '[]' : '·'}
${isLocked
- ? `
-
- 🔒
- `
+ ? ` `
: input(row.name.value, `data-path="${path}.name.value"`, { extra: 'flex-1' })}
|
+ ${input(row.description.value, `data-path="${path}.description.value"`, { placeholder: '설명' })} |
${row.type.locked
- ? lockedInput(row.type.value)
+ ? ``
: selectInput(row.type.value,
['string', 'integer', 'number', 'boolean', 'array', 'object'],
`data-path="${path}.type.value" data-rerender="step3"`)}
|
-
- ${checkbox(!!row.required.value, `data-path="${path}.required.value" data-type="bool"`, { locked: row.required.locked })}
- |
-
- ${isObject || isArray
- ? '—'
- : selectInput(row.format.value, ['', 'uuid', 'date', 'date-time', 'email', 'uri', 'int32', 'int64', 'float', 'double', 'byte', 'binary'], `data-path="${path}.format.value"`)}
- |
${row.type.value === 'string'
? input(row.maxLength.value, `data-path="${path}.maxLength.value"`, { placeholder: '0', extra: 'text-xs' })
: '—'}
|
-
+ ${checkbox(!!row.required.value, `data-path="${path}.required.value" data-type="bool"`, { locked: row.required.locked })}
|
- ${input(row.description.value, `data-path="${path}.description.value"`, { placeholder: '설명' })} |
${isObject ? '— (자식 참조)' :
isArray
@@ -623,52 +701,20 @@
items:
${selectInput((row.itemsType && row.itemsType.value) || 'string', ['string', 'integer', 'number', 'boolean'], `data-path="${path}.itemsType.value"`, { extra: 'text-xs' })}
`
- : input(row.example.value, `data-path="${path}.example.value"`, { placeholder: '예제' })}
+ : input(row.example.value, `data-path="${path}.example.value"`, { placeholder: '예제값' })}
|
${isLocked
- ? '🔒'
+ ? ''
: ``}
|
`;
- // 제약 expand 행
- if (showConstraints) {
- html += `
-
-
-
-
-
- ${input(row.pattern.value, `data-path="${path}.pattern.value"`, { placeholder: '^[A-Z]+$', extra: 'text-xs' })}
-
-
-
- ${input((row.enumStr && row.enumStr.value) || '', `data-path="${path}.enumStr.value"`, { placeholder: 'A,B,C', extra: 'text-xs' })}
-
-
-
- ${input((row.minimum && row.minimum.value) || '', `data-path="${path}.minimum.value"`, { placeholder: '0', extra: 'text-xs' })}
-
-
-
- ${input((row.maximum && row.maximum.value) || '', `data-path="${path}.maximum.value"`, { placeholder: '100', extra: 'text-xs' })}
-
-
-
- ${input((row.default && row.default.value) || '', `data-path="${path}.default.value"`, { placeholder: '기본값', extra: 'text-xs' })}
-
-
- |
-
- `;
- }
-
// children
if (isObject && isExpanded) {
if (row.children && row.children.length) {
- html += renderSchemaRows(row.children, `${path}.children`, depth + 1);
+ html += renderSchemaRows(row.children, `${path}.children`, depth + 1, seq);
}
html += `
@@ -683,11 +729,11 @@
}
// ============================================================
- // 9. Step 4 — 인증 / 보안
+ // 9. 인증 / 보안 (기본정보 Step 에 병합된 섹션)
// ============================================================
- function renderStep4(root) {
+ function renderAuthSection() {
var schemes = state.data.securitySchemes || [];
- root.innerHTML = card(
+ return card(
sectionTitle('인증 / 보안', '게이트웨이(GW)에 설정된 인증만 표시됩니다. (읽기 전용)') +
(schemes.length
? schemes.map((s, i) => renderSecuritySchemeCard(s, i)).join('')
@@ -737,61 +783,164 @@
}
// ============================================================
- // 10. Step 5 — 예제 / 응답
+ // 10. Step 4 — 요청/응답 샘플 (하위 탭: 요청=편집, 응답=읽기전용(응답 정보에서 파생))
// ============================================================
- function renderStep5(root) {
- const reqObj = state.data.examples.request['application/json'] || { default: '' };
- const req = reqObj.default || '';
- const codes = Object.keys(state.data.examples.response);
- const cur = state.responseStatusTab;
- const curSet = state.data.examples.response[cur] || { body: '', headers: [] };
- const taCls = 'w-full px-3 py-2 text-xs font-mono rounded border border-slate-300 focus:outline-none focus:ring-2 focus:ring-brand-500';
+ // 응답 예제 본문을 응답 정보(스키마·예제값)에서 파생해 동기화 — 응답 샘플은 항상 응답 정보 세팅을 따른다.
+ function syncResponseExamplesFromSchema() {
+ var d = state.data;
+ Object.keys(d.responses || {}).forEach(function (code) {
+ var set = d.examples.response[code] || (d.examples.response[code] = { body: '', headers: [] });
+ set.body = JSON.stringify(_sampleObj((d.responses[code] || {}).schema || []), null, 2);
+ });
+ }
- root.innerHTML = `
- ${card(
+ function renderStep5(root) {
+ const tab = state.sampleTab || 'request';
+ const tabs = [{ key: 'request', label: '요청' }, { key: 'response', label: '응답' }];
+ const taCls = 'w-full px-3 py-2 text-xs font-mono rounded border border-slate-300 focus:outline-none focus:ring-2 focus:ring-brand-500';
+ let body;
+ if (tab === 'request') {
+ const _mt = (state.data.requestBody && state.data.requestBody.mediaType) || 'application/json';
+ const req = (state.data.examples.request[_mt] || { default: '' }).default || '';
+ body = card(
sectionTitle('요청 본문 예제 (application/json)') +
- `` +
+ `` +
`` +
`스키마와 자동 매핑되어 Swagger UI 의 예제 영역에 노출됩니다.
`
- )}
-
- ${card(
- sectionTitle('응답 예제', '상태코드별 본문/헤더 1세트. 기본 200 + 추가 등록 가능.') +
+ );
+ } else {
+ syncResponseExamplesFromSchema(); // 응답 정보 세팅 → 예제 파생(읽기 전용)
+ const codes = Object.keys(state.data.examples.response);
+ const cur = state.responseStatusTab;
+ const curSet = state.data.examples.response[cur] || { body: '', headers: [] };
+ body = card(
+ sectionTitle('응답 예제 (읽기 전용)', '응답 정보(스키마·예제값)에서 자동 생성됩니다. 수정은 [응답 정보] 탭에서 하세요.') +
`
${codes.map(code => `
+ ${cur === code ? 'border-brand-600 text-brand-700 font-medium' : 'border-transparent text-slate-600'}">${escapeHtml(code)}
`).join('')}
-
` +
- `` +
- `` +
- `
-
-
-
- | name | type | description | example | |
-
-
- ${(curSet.headers || []).map((h, i) => `
-
- | ${input(h.name, `data-resp-header="${i}.name"`)} |
- ${selectInput(h.type, ['string', 'integer', 'number', 'boolean'], `data-resp-header="${i}.type"`)} |
- ${input(h.description, `data-resp-header="${i}.description"`)} |
- ${input(h.example, `data-resp-header="${i}.example"`)} |
- |
-
- `).join('')}
-
-
-
-
-
-
`
- )}
- `;
+ `` +
+ `` +
+ `응답 헤더는 [응답 정보 > Header] 에서 관리합니다.
`
+ );
+ }
+ root.innerHTML = subTabBar(tabs, tab, 'data-sample-tab') + body;
+ initExampleEditors(tab); // 요청=편집 CodeMirror, 응답=읽기전용 CodeMirror
+ }
+
+ // (구 요청/응답(웹페이지) 스텝은 요청/응답 정보의 'API 설명(HTML)' 서브 탭으로 흡수됨)
+
+ // 스키마 → 설명 자료 HTML 표(필드명/타입/필수/길이/설명). 현재 편집 중 스키마(추가 필드 포함) 기준.
+ function schemaToSpecHtml(rows) {
+ var out = ''
+ + '| 필드명 | '
+ + '타입 | '
+ + '필수 | '
+ + '길이 | '
+ + '설명 |
';
+ function walk(list, depth) {
+ (list || []).forEach(function (r) {
+ if (!r || !r.name) return;
+ var pad = new Array(depth + 1).join(' ');
+ var req = 'Y'; // 설명 자료 HTML: 필수 여부 기본값 = 필수(Y)
+ var len = (r.maxLength && r.maxLength.value) || '';
+ // 필드명·설명 = 좌측 정렬, 나머지(타입·필수·길이) = 가운데 정렬
+ out += ''
+ + '| ' + pad + escapeHtml(r.name.value || '') + ' | '
+ + '' + escapeHtml(r.type.value || '') + ' | '
+ + '' + req + ' | '
+ + '' + escapeHtml(String(len)) + ' | '
+ + '' + escapeHtml((r.description && r.description.value) || '') + ' |
';
+ if (r.children && r.children.length) walk(r.children, depth + 1);
+ });
+ }
+ walk(rows, 0);
+ out += '
';
+ return out;
+ }
+ function regenReqSpec() {
+ var d = state.data;
+ d.msgSpec.request = schemaToSpecHtml(d.requestBody ? d.requestBody.schema : []);
+ renderForm();
+ toast('요청 설명 자료를 스키마 기준으로 재생성했습니다');
+ }
+ function regenResSpec() {
+ var d = state.data;
+ var r = (d.responses && (d.responses['200'] || d.responses[Object.keys(d.responses)[0]])) || {};
+ d.msgSpec.response = schemaToSpecHtml(r.schema || []);
+ renderForm();
+ toast('응답 설명 자료를 스키마 기준으로 재생성했습니다');
+ }
+
+ // 개발자포탈과 동일한 Summernote 툴바 + 편집영역(.editor-content) 스타일
+ var DJB_SN_TOOLBAR = [
+ ['style', ['style']],
+ ['font', ['bold', 'underline', 'clear']],
+ ['color', ['color']],
+ ['para', ['ul', 'ol', 'paragraph']],
+ ['table', ['table']],
+ ['insert', ['link', 'picture']],
+ ['view', ['fullscreen', 'codeview', 'help']]
+ ];
+ function addEditorContentClass() { if (window.jQuery) window.jQuery('.note-editable').addClass('editor-content'); }
+
+ // 설명 자료 = Summernote 리치 에디터. state.data.msgSpec[key] 에 HTML 저장.
+ function initSpecEditor(sel, key, height) {
+ if (!(window.jQuery && window.jQuery.fn && window.jQuery.fn.summernote)) return;
+ var $e = window.jQuery(sel);
+ if (!$e.length) return;
+ try { $e.summernote('destroy'); } catch (e) {}
+ var koOk = window.jQuery.summernote && window.jQuery.summernote.lang && window.jQuery.summernote.lang['ko-KR'];
+ $e.summernote({
+ height: height || 300,
+ lang: koOk ? 'ko-KR' : 'en-US',
+ placeholder: '여기에 내용을 입력하세요.',
+ toolbar: DJB_SN_TOOLBAR,
+ callbacks: {
+ onInit: addEditorContentClass,
+ onChange: function (contents) { state.data.msgSpec[key] = contents; }
+ }
+ });
+ $e.summernote('code', state.data.msgSpec[key] || '');
+ addEditorContentClass();
+ }
+
+ // 요청/응답 본문 예제 textarea 를 CodeMirror(JSON) 에디터로 승격. renderStep5 마다 재생성(폼 재렌더 시 wipe→재초기화).
+ // 요청 = 편집 가능, 응답 = 읽기 전용(응답 정보에서 파생).
+ function initExampleEditors() {
+ if (!window.CodeMirror) return;
+ var cmOpt = { mode: { name: 'javascript', json: true }, lineNumbers: true, lineWrapping: true, viewportMargin: Infinity, tabSize: 2 };
+ var reqTa = $('[data-example-target="request"]');
+ if (reqTa) {
+ var cmReq = CodeMirror.fromTextArea(reqTa, cmOpt);
+ cmReq.setSize('100%', 300);
+ cmReq.on('change', function (c) {
+ var mt = (state.data.requestBody && state.data.requestBody.mediaType) || 'application/json';
+ state.data.examples.request[mt] = state.data.examples.request[mt] || { default: '' };
+ state.data.examples.request[mt].default = c.getValue();
+ schedulePreviewSync();
+ });
+ }
+ var resTa = $('[data-example-target="response"]');
+ if (resTa) {
+ var cmRes = CodeMirror.fromTextArea(resTa, Object.assign({}, cmOpt, { readOnly: true }));
+ cmRes.setSize('100%', 300);
+ }
+ }
+
+ // 응답유형별 실제 호출 주소. 경로(op.path.value)에 어댑터경로가 이미 포함, 호스트만 응답유형별로 앞에 붙임.
+ function resolvedCallUrl() {
+ var d = state.data;
+ var rt = (d.portal && d.portal.responseType) || 'sample';
+ var host;
+ if (rt === 'gw') host = (window.DJB_CTX && window.DJB_CTX.gwAddress) || 'http://127.0.0.1:39310';
+ else if (rt === 'mock') host = (d.portal && d.portal.mockUrl) || '';
+ else host = '';
+ var path = (d.operation && d.operation.path && d.operation.path.value) || '';
+ if (!host) return path || '(주소 없음)';
+ return host.replace(/\/+$/, '') + (path.charAt(0) === '/' ? path : '/' + path);
}
// ============================================================
@@ -805,9 +954,20 @@
const orgChips = (p.orgs || []).map((o, i) =>
`${escapeHtml(o.orgName || o.id)}✕`
).join('');
+ const groupChips = (p.apiGroups || []).map((g, i) =>
+ `${escapeHtml(g.groupName || g.id)}✕`
+ ).join('');
root.innerHTML =
card(
sectionTitle('포탈 게시 정보', '개발자포탈 공개 설정. 기존 API 스펙 화면의 값을 불러와 적용합니다.') +
+ fieldRow('API 그룹',
+ `
+
+ ${groupChips || '선택된 그룹 없음'}
+
+
+
API 그룹은 apiGroupMan(API 그룹 관리)에서도 관리됩니다.
+
`) +
fieldRow('공개 여부', ``) +
fieldRow('공개 권한',
`
@@ -819,15 +979,26 @@
전부 선택 시 로그인 사용자 전체에게 공개됩니다.
`) +
(showOrg ? fieldRow('공개 법인',
- `
-
${orgChips || '선택된 법인 없음'}
-
+ `
+
+ ${orgChips || '선택된 법인 없음'}
+
+
+
특정 법인에게만 공개하고 싶을 때 공개 법인을 선택하시면 됩니다.
`) : '') +
fieldRow('응답 유형',
+ '
' +
['sample', 'mock', 'gw'].map(v =>
`
`
- ).join('')) +
- (rt === 'mock' ? fieldRow('Mock URL', input(p.mockUrl || '', 'data-portal="mockUrl"', { placeholder: 'https://...' })) : '')
+ ).join('') +
+ `
실제 호출 주소: ${escapeHtml(resolvedCallUrl())}
` +
+ (rt === 'gw' ? `
GW 주소는 포탈 설정(PTL_PROPERTY, 그룹 Portal)의 djb.gateway.base-url 값을 사용합니다.
` : '') +
+ '
') +
+ (rt === 'mock' ? fieldRow('Mock Server URL', input(p.mockUrl || '', 'data-portal="mockUrl"', { placeholder: 'https://mock.example.com' }), { hint: 'Mock 응답을 제공할 서버의 기본 URL. 응답 유형이 Mock 일 때 개발자포탈이 이 주소로 요청을 전달하고, Swagger 스펙의 서버 주소로도 사용됩니다.' }) : '')
+ ) +
+ card(
+ sectionTitle('설명 (API 소개 페이지)', 'API 소개 페이지에만 표시됩니다. OpenAPI 스펙 파일에는 포함되지 않습니다.') +
+ '
' + (state.data.info.description.value || '') + '
'
) +
card(
sectionTitle('내보내기', 'OpenAPI 스펙을 파일로 저장합니다.') +
@@ -836,6 +1007,7 @@
`
);
+ initInfoDescEditor(); // 설명 리치 에디터 (API 소개 페이지 전용)
}
// ============================================================
@@ -983,15 +1155,7 @@
return;
}
- // Response status tab
- const resTab = target.closest('[data-res-tab]');
- if (resTab) {
- state.responseStatusTab = resTab.dataset.resTab;
- renderStep3Section();
- return;
- }
-
- // 상태코드 삭제 (탭 안의 ✕) — 탭 전환보다 먼저 처리
+ // 상태코드 삭제 (탭 안의 ✕) — 탭 전환(data-res-tab / data-ex-res-tab)보다 먼저 처리
const delStatus = target.closest('[data-del-status]');
if (delStatus) {
const dcode = delStatus.dataset.delStatus;
@@ -1004,6 +1168,14 @@
}
return;
}
+
+ // Response status tab
+ const resTab = target.closest('[data-res-tab]');
+ if (resTab) {
+ state.responseStatusTab = resTab.dataset.resTab;
+ renderStep3Section();
+ return;
+ }
// 상태코드 추가
if (target.id === 'btn-add-status') {
const code = (window.prompt('추가할 HTTP 상태코드 (100~599)', '400') || '').trim();
@@ -1026,6 +1198,16 @@
return;
}
+ // 요청/응답 샘플 서브 탭 전환
+ const sampleTabBtn = target.closest('[data-sample-tab]');
+ if (sampleTabBtn) { state.sampleTab = sampleTabBtn.dataset.sampleTab; renderForm(); return; }
+
+ // 요청/응답 정보 서브 탭 전환
+ const reqInfoTabBtn = target.closest('[data-reqinfo-tab]');
+ if (reqInfoTabBtn) { state.reqInfoTab = reqInfoTabBtn.dataset.reqinfoTab; renderForm(); return; }
+ const resInfoTabBtn = target.closest('[data-resinfo-tab]');
+ if (resInfoTabBtn) { state.resInfoTab = resInfoTabBtn.dataset.resinfoTab; renderForm(); return; }
+
// 행 추가
const addRow = target.closest('[data-add-row]');
if (addRow) {
@@ -1080,8 +1262,11 @@
schedulePreviewSync();
return;
}
- if (target.id === 'btn-add-param') {
- state.data.parameters.push(newParamRow());
+ const addParam = target.closest('[data-add-param]');
+ if (addParam) {
+ var np = newParamRow();
+ np.in = addParam.dataset.addParam || 'query';
+ state.data.parameters.push(np);
renderStep3Section();
schedulePreviewSync();
return;
@@ -1091,7 +1276,7 @@
const delSec = target.closest('[data-del-sec]');
if (delSec) {
state.data.securitySchemes.splice(Number(delSec.dataset.delSec), 1);
- renderStep4($('#form-content'));
+ renderStep1($('#form-content'));
schedulePreviewSync();
return;
}
@@ -1118,6 +1303,10 @@
if (target.id === 'btn-regen-req-ex') { regenReqExample(); return; }
if (target.id === 'btn-regen-res-ex') { regenResExample(); return; }
+ // 설명 자료 파라미터/스키마 기준 재생성
+ if (target.id === 'btn-regen-req-spec') { regenReqSpec(); return; }
+ if (target.id === 'btn-regen-res-spec') { regenResSpec(); return; }
+
// 공개 법인 선택/삭제
if (target.id === 'btn-select-org') { openOrgPopup(); return; }
const delOrg = target.closest('[data-del-org]');
@@ -1128,6 +1317,15 @@
return;
}
+ // API 그룹 선택/삭제
+ if (target.id === 'btn-select-group') { openGroupPopup(); return; }
+ const delGroup = target.closest('[data-del-group]');
+ if (delGroup) {
+ state.data.portal.apiGroups.splice(Number(delGroup.dataset.delGroup), 1);
+ renderForm();
+ return;
+ }
+
// Export
const exportBtn = target.closest('[data-export]');
if (exportBtn) {
@@ -1176,7 +1374,9 @@
form.addEventListener('input', e => {
const t = e.target;
if (t.dataset.exampleTarget === 'request') {
- state.data.examples.request['application/json'].default = t.value;
+ var _mt = (state.data.requestBody && state.data.requestBody.mediaType) || 'application/json';
+ state.data.examples.request[_mt] = state.data.examples.request[_mt] || { default: '' };
+ state.data.examples.request[_mt].default = t.value;
schedulePreviewSync();
}
if (t.dataset.exampleTarget === 'response') {
@@ -1191,7 +1391,7 @@
}
// 포탈 게시 정보 (공개법인 / Mock URL)
if (t.dataset.portal === 'displayOrg') { state.data.portal.displayOrg = t.value; }
- if (t.dataset.portal === 'mockUrl') { state.data.portal.mockUrl = t.value; }
+ if (t.dataset.portal === 'mockUrl') { state.data.portal.mockUrl = t.value; var _u = $('#resolved-call-url'); if (_u) _u.textContent = resolvedCallUrl(); }
});
}
function newSchemaRow() {
@@ -1312,7 +1512,7 @@
if (chosenType === 'openIdConnect') newS.openIdConnectUrl = { value: $('#sec-oidc').value, locked: false };
state.data.securitySchemes.push(newS);
modal.classList.add('hidden');
- renderStep4($('#form-content'));
+ renderStep1($('#form-content'));
schedulePreviewSync();
toast(`보안 스킴 "${key}" 추가 완료`);
};
@@ -1432,6 +1632,14 @@
var bRegen = $('#btn-regen'); if (bRegen) bRegen.addEventListener('click', regenerate);
var bClose = $('#btn-close'); if (bClose) bClose.addEventListener('click', () => window.close());
+ // Cmd/Ctrl + S → 저장 (브라우저 기본 저장 대화상자 차단)
+ document.addEventListener('keydown', function (e) {
+ if ((e.metaKey || e.ctrlKey) && (e.key === 's' || e.key === 'S')) {
+ e.preventDefault();
+ saveSpec(false);
+ }
+ });
+
// iframe(모달)로 열렸을 때만 "새창으로 띄우기" 노출 — 현재 URL 을 새 창으로 열고 모달 닫기
if (window.self !== window.top) {
var bNew = $('#btn-newwin');
@@ -1474,7 +1682,8 @@
var d = deepClone(window.SAMPLE_DATA);
if (!spec || typeof spec !== 'object') return d;
d.__openapi = spec.openapi || '3.1.0'; // openapi 버전 보존(3.0/3.1 모두), 기본 3.1.0
- d.portal = { displayYn: 'Y', roles: { ROLE_USER: false, ROLE_CORP_USER: false, ROLE_CORP_MANAGER: false }, displayOrg: '', orgs: [], responseType: 'sample', mockUrl: '' };
+ d.portal = { displayYn: 'Y', roles: { ROLE_USER: false, ROLE_CORP_USER: false, ROLE_CORP_MANAGER: false }, displayOrg: '', orgs: [], apiGroups: [], responseType: 'sample', mockUrl: '' };
+ d.msgSpec = { request: '', response: '' }; // 요청/응답 메시지 스펙(HTML, API 소개 페이지용) = apiRequestSpec/apiResponseSpec
var info = spec.info || {};
d.info.title = wrap(info.title || '', false);
d.info.version = wrap(info.version || '1.0.0', false);
@@ -1543,16 +1752,34 @@
}
// 스키마 기반 샘플 JSON 생성 (자동생성 예제)
- function _primVal(t) { if (t === 'integer' || t === 'number') return 0; if (t === 'boolean') return false; return 'string'; }
function _coerce(t, v) { if (t === 'integer') return parseInt(v, 10) || 0; if (t === 'number') return parseFloat(v) || 0; if (t === 'boolean') return v === true || v === 'true'; return v; }
+ // 기본 예제값: string→sample_명(길이 지정 시 맞춰 자름), integer→123456, number→123456.123, boolean→false
+ // - 숫자는 길이 지정 시 정수부를 그 길이에 맞춰 자름(실수는 소수 .123 유지)
+ function _defScalar(t, name, maxLen) {
+ var ml = parseInt(maxLen, 10);
+ if (t === 'integer') {
+ var iv = '123456';
+ if (ml && ml > 0 && iv.length > ml) iv = iv.substring(0, ml);
+ return parseInt(iv, 10);
+ }
+ if (t === 'number') {
+ var ip = '123456';
+ if (ml && ml > 0 && ip.length > ml) ip = ip.substring(0, ml);
+ return parseFloat(ip + '.123');
+ }
+ if (t === 'boolean') return false;
+ var v = 'sample_' + (name || '');
+ if (ml && ml > 0 && v.length > ml) v = v.substring(0, ml);
+ return v;
+ }
function _sampleVal(row) {
var t = row.type.value, ex = row.example && row.example.value;
if (t === 'object') return _sampleObj(row.children || []);
- if (t === 'array') { var it = (row.itemsType && row.itemsType.value) || 'string'; return [it === 'object' ? _sampleObj(row.children || []) : _primVal(it)]; }
+ if (t === 'array') { var it = (row.itemsType && row.itemsType.value) || 'string'; return [it === 'object' ? _sampleObj(row.children || []) : _defScalar(it, (row.name && row.name.value) || 'item', '')]; }
if (ex !== undefined && ex !== '') return _coerce(t, ex);
- return _primVal(t);
+ return _defScalar(t, (row.name && row.name.value) || '', row.maxLength && row.maxLength.value);
}
- function _sampleObj(rows) { var o = {}; (rows || []).forEach(function (r) { o[r.name.value] = _sampleVal(r); }); return o; }
+ function _sampleObj(rows) { var o = {}; (rows || []).forEach(function (r) { if (r && r.name && r.name.value) o[r.name.value] = _sampleVal(r); }); return o; }
function ensureExamplesFromSchema(force) {
var d = state.data;
d.examples = d.examples || { request: {}, response: {}, responseHeaders: [] };
@@ -1568,59 +1795,105 @@
}
}
- // 예제 재생성 — 자동생성과 동일 규칙(백엔드 GENERATE_SAMPLE = generateSampleDataFromLayout)
+ // 예제 재생성 — 현재 편집 중(메모리) 스키마 기준 + 자동생성과 동일 값 규칙(D10). 추가/미저장 필드 반영.
function regenReqExample() {
- var ctx = window.DJB_CTX || {};
- if (!ctx.jsonUrl || !ctx.eaiSvcName) return;
- fetch(ctx.jsonUrl + '?cmd=GENERATE_SAMPLE&serviceType=APIGW', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: toForm({ eaiSvcName: ctx.eaiSvcName, layoutType: 'REQUEST' }) })
- .then(r => r.json())
- .then(function (res) {
- var mt = (state.data.requestBody && state.data.requestBody.mediaType) || 'application/json';
- state.data.examples.request = state.data.examples.request || {};
- state.data.examples.request[mt] = { default: res.json || '{}' };
- renderForm(); schedulePreviewSync();
- toast('요청 예제 재생성(자동생성 규칙)');
- })
- .catch(e => toast('재생성 실패: ' + e.message, 'error'));
+ var d = state.data;
+ var mt = (d.requestBody && d.requestBody.mediaType) || 'application/json';
+ d.examples.request = d.examples.request || {};
+ d.examples.request[mt] = { default: JSON.stringify(_sampleObj(d.requestBody ? d.requestBody.schema : []), null, 2) };
+ renderForm(); schedulePreviewSync();
+ toast('요청 예제 재생성(현재 스키마 기준)');
}
function regenResExample() {
- var ctx = window.DJB_CTX || {};
- if (!ctx.jsonUrl || !ctx.eaiSvcName) return;
+ var d = state.data;
var code = state.responseStatusTab;
- fetch(ctx.jsonUrl + '?cmd=GENERATE_SAMPLE&serviceType=APIGW', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: toForm({ eaiSvcName: ctx.eaiSvcName, layoutType: 'RESPONSE' }) })
- .then(r => r.json())
- .then(function (res) {
- var set = state.data.examples.response[code] || (state.data.examples.response[code] = { body: '', headers: [] });
- set.body = res.json || '{}';
- renderForm(); schedulePreviewSync();
- toast(code + ' 응답 예제 재생성(자동생성 규칙)');
- })
- .catch(e => toast('재생성 실패: ' + e.message, 'error'));
+ var r = d.responses[code] || {};
+ var set = d.examples.response[code] || (d.examples.response[code] = { body: '', headers: [] });
+ set.body = JSON.stringify(_sampleObj(r.schema || []), null, 2);
+ renderForm(); schedulePreviewSync();
+ toast(code + ' 응답 예제 재생성(현재 스키마 기준)');
+ }
+
+ // 스키마 각 스칼라 필드의 '예제값'(row.example.value) 을 기본 규칙(_defScalar)으로 채움(재귀).
+ function fillSchemaExamples(rows) {
+ (rows || []).forEach(function (r) {
+ if (!r || !r.name) return;
+ var t = r.type.value;
+ if (t === 'object' || t === 'array') {
+ if (r.children && r.children.length) fillSchemaExamples(r.children);
+ } else {
+ r.example = r.example || { value: '', locked: false };
+ r.example.value = String(_defScalar(t, r.name.value || '', r.maxLength && r.maxLength.value));
+ }
+ });
+ }
+
+ // 자동생성(초기화) 시 백엔드가 준 기초 스펙 위에, 추가자료(예제·설명자료)를 프론트에서 스키마 기준 생성.
+ // 재생성 버튼(_sampleObj / schemaToSpecHtml)과 동일 로직 → 자동생성 결과 == 재생성 결과.
+ function frontAutoGenerate() {
+ var d = state.data;
+ var mt = (d.requestBody && d.requestBody.mediaType) || 'application/json';
+ // 1) 스키마 필드별 예제값 채움 (요청 + 모든 응답)
+ fillSchemaExamples(d.requestBody ? d.requestBody.schema : []);
+ Object.keys(d.responses || {}).forEach(function (code) { fillSchemaExamples((d.responses[code] || {}).schema || []); });
+ // 2) 요청/응답 본문 예제 JSON (채워진 예제값 사용)
+ d.examples = d.examples || { request: {}, response: {} };
+ d.examples.request = d.examples.request || {};
+ d.examples.request[mt] = { default: JSON.stringify(_sampleObj(d.requestBody ? d.requestBody.schema : []), null, 2) };
+ d.examples.response = d.examples.response || {};
+ Object.keys(d.responses || {}).forEach(function (code) {
+ var set = d.examples.response[code] || (d.examples.response[code] = { body: '', headers: [] });
+ set.body = JSON.stringify(_sampleObj((d.responses[code] || {}).schema || []), null, 2);
+ });
+ d.msgSpec = d.msgSpec || { request: '', response: '' };
+ d.msgSpec.request = schemaToSpecHtml(d.requestBody ? d.requestBody.schema : []);
+ var r200 = (d.responses && (d.responses['200'] || d.responses[Object.keys(d.responses)[0]])) || {};
+ d.msgSpec.response = schemaToSpecHtml(r200.schema || []);
}
// 공개 법인 선택 팝업(기존 apiSpecManDisplayOrgPopup 재사용) — 자체 iframe 모달. 팝업이 window.parent.selectOrg 호출.
+ // 공통 iframe 피커 모달(법인/그룹). 팝업이 same-origin 이라 window.parent. 호출 + close shim 으로 닫기.
+ function openPickerModal(url, title) {
+ closePickerModal();
+ var ov = document.createElement('div');
+ ov.id = 'picker-modal';
+ ov.style.cssText = 'position:fixed;inset:0;background:rgba(15,23,42,0.45);z-index:1000;display:flex;align-items:center;justify-content:center;';
+ ov.innerHTML = ''
+ + '
' + escapeHtml(title) + '
'
+ + '
';
+ document.body.appendChild(ov);
+ ov.addEventListener('click', function (e) { if (e.target === ov) closePickerModal(); });
+ var x = ov.querySelector('#picker-modal-x'); if (x) x.addEventListener('click', closePickerModal);
+ var ifr = ov.querySelector('iframe');
+ if (ifr) ifr.addEventListener('load', function () { try { ifr.contentWindow.close = function () { closePickerModal(); }; } catch (e) {} });
+ }
+ function closePickerModal() { var ov = document.getElementById('picker-modal'); if (ov && ov.parentNode) ov.parentNode.removeChild(ov); }
+
function openOrgPopup() {
var ctx = window.DJB_CTX || {};
if (!ctx.orgPopupUrl) { toast('법인 팝업을 열 수 없습니다', 'error'); return; }
var menuId = new URLSearchParams(location.search).get('menuId') || '';
var ids = (state.data.portal.orgs || []).map(function (o) { return o.id; }).join(',');
- var url = ctx.orgPopupUrl + '?cmd=POPUP_LIST&serviceType=APIGW&menuId=' + encodeURIComponent(menuId) + '&selectedOrgIds=' + encodeURIComponent(ids);
- var ov = document.createElement('div');
- ov.id = 'org-modal';
- ov.style.cssText = 'position:fixed;inset:0;background:rgba(15,23,42,0.45);z-index:1000;display:flex;align-items:center;justify-content:center;';
- ov.innerHTML = '';
- document.body.appendChild(ov);
- ov.addEventListener('click', function (e) { if (e.target === ov) closeOrgPopup(); });
- var x = ov.querySelector('#org-modal-x'); if (x) x.addEventListener('click', closeOrgPopup);
+ openPickerModal(ctx.orgPopupUrl + '?cmd=POPUP_LIST&serviceType=APIGW&menuId=' + encodeURIComponent(menuId) + '&selectedOrgIds=' + encodeURIComponent(ids), '공개 법인 선택');
}
- function closeOrgPopup() { var ov = document.getElementById('org-modal'); if (ov && ov.parentNode) ov.parentNode.removeChild(ov); }
window.selectOrg = function (orgs) {
var p = state.data.portal;
p.orgs = orgs || [];
p.displayOrg = p.orgs.map(function (o) { return o.id; }).join(',');
- closeOrgPopup();
+ closePickerModal();
+ renderForm();
+ };
+
+ function openGroupPopup() {
+ var ctx = window.DJB_CTX || {};
+ if (!ctx.groupPopupUrl) { toast('그룹 팝업을 열 수 없습니다', 'error'); return; }
+ var menuId = new URLSearchParams(location.search).get('menuId') || '';
+ var ids = (state.data.portal.apiGroups || []).map(function (g) { return g.id; }).join(',');
+ openPickerModal(ctx.groupPopupUrl + '&serviceType=APIGW&menuId=' + encodeURIComponent(menuId) + '&selectedGroupIds=' + encodeURIComponent(ids), 'API 그룹 선택');
+ }
+ window.selectApiGroup = function (groups) {
+ state.data.portal.apiGroups = groups || [];
+ closePickerModal();
renderForm();
};
@@ -1649,6 +1922,13 @@
}
if (res.responseType) pp.responseType = res.responseType;
if (res.mockUrl) pp.mockUrl = decodeEntities(res.mockUrl);
+ if (res.apiGroups) { try { pp.apiGroups = JSON.parse(res.apiGroups) || []; } catch (e) { pp.apiGroups = []; } }
+ // 요청/응답 메시지 스펙(HTML) — 저장본은 저장값, 자동생성분은 프론트에서 생성
+ 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();
})
.catch(e => console.error('loadInitialData', e));
}
@@ -1671,12 +1951,16 @@
testbedSpec: JSON.stringify(spec),
sampleRequest: reqEx,
sampleResponse: resEx,
+ // 요청/응답 메시지 스펙(HTML, API 소개 페이지용)
+ apiRequestSpec: (d.msgSpec && d.msgSpec.request) || '',
+ apiResponseSpec: (d.msgSpec && d.msgSpec.response) || '',
// 포탈 게시 정보
displayYn: (d.portal && d.portal.displayYn) || 'N',
displayRoleCode: d.portal ? Object.keys(d.portal.roles).filter(function (k) { return d.portal.roles[k]; }).join(',') : '',
displayOrg: (d.portal && d.portal.displayOrg) || '',
responseType: (d.portal && d.portal.responseType) || 'sample',
- mockUrl: (d.portal && d.portal.mockUrl) || ''
+ mockUrl: (d.portal && d.portal.mockUrl) || '',
+ apiGroupIds: (d.portal && d.portal.apiGroups) ? d.portal.apiGroups.map(function (g) { return g.id; }).join(',') : ''
};
fetch(ctx.saveUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: toForm(params) })
.then(r => r.json())
@@ -1701,7 +1985,9 @@
try { state.data = specToData(JSON.parse(res.testbedSpec)); }
catch (e) { toast('자동 생성 실패: 스펙 파싱 오류', 'error'); return; }
}
- // 자동생성 규칙(title/tag/summary=API명, 예제, 서버prepend, opId)은 백엔드(generateSpec)에서 적용됨 → 그대로 로드
+ // 구조(title/tag/summary/opId/스키마/보안/헤더)는 백엔드(generateSpec)가 적용한 기초 스펙 그대로 로드.
+ // 추가자료(예제·설명자료)는 프론트에서 스키마 기준 생성 → 재생성 버튼과 동일 결과.
+ frontAutoGenerate();
state.step = 1;
state.responseStatusTab = state.data.responses && state.data.responses['200'] ? '200' : (state.data.responses ? (Object.keys(state.data.responses)[0] || '200') : '200');
var badge = $('#hdr-source'); if (badge) { badge.textContent = '자동생성'; badge.classList.remove('hidden'); }
diff --git a/WebContent/js/djb/apispec/editor-content.css b/WebContent/js/djb/apispec/editor-content.css
new file mode 100644
index 0000000..c144110
--- /dev/null
+++ b/WebContent/js/djb/apispec/editor-content.css
@@ -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;
+ }
+}
diff --git a/WebContent/jsp/onl/apim/apigroup/apiGroupManDetail.jsp b/WebContent/jsp/onl/apim/apigroup/apiGroupManDetail.jsp
index a4a507f..7bd7487 100644
--- a/WebContent/jsp/onl/apim/apigroup/apiGroupManDetail.jsp
+++ b/WebContent/jsp/onl/apim/apigroup/apiGroupManDetail.jsp
@@ -23,7 +23,7 @@
+
+
+
+
+