/* DJB OpenAPI Editor POC — app.js
* vanilla JS, 단일 상태 + 함수형 렌더
*/
(function () {
'use strict';
// ============================================================
// 1. 상태
// ============================================================
const STEPS = [
{ 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 = {
step: 1,
showPreview: true,
previewTab: 'swagger', // 'swagger' | 'yaml-ro' | 'yaml-edit'
data: deepClone(window.SAMPLE_DATA),
step3Section: 'body', // 'params' | 'body' | 'response' | 'components'
responseStatusTab: '200',
components: [], // 재사용 스키마(POC: 빈 상태)
};
// GW authtype(oauth/api_key 등, 서버 DETAIL_SPEC 응답). 인증 badge 라벨 판별용.
// (enricher 는 oauth·api_key 를 모두 apiKey-in-header 로 모델링하므로 scheme.type 만으로 구분 불가)
var djbAuthType = '';
// 마지막 저장(또는 최초 로드) 시점의 state.data 스냅샷 — 닫기 전 미저장 변경 여부 판별용.
var savedSnapshot = null;
function isDirty() { return savedSnapshot !== null && JSON.stringify(state.data) !== savedSnapshot; }
function closeEditor() {
if (isDirty() && !window.confirm('저장하지 않은 변경사항이 있습니다. 저장하지 않고 닫으시겠습니까?')) return;
window.close();
}
// ============================================================
// 2. 유틸
// ============================================================
function deepClone(o) { return JSON.parse(JSON.stringify(o)); }
function $(sel, ctx) { return (ctx || document).querySelector(sel); }
function $$(sel, ctx) { return Array.from((ctx || document).querySelectorAll(sel)); }
function el(tag, attrs, children) {
const e = document.createElement(tag);
if (attrs) for (const k in attrs) {
if (k === 'class') e.className = attrs[k];
else if (k === 'html') e.innerHTML = attrs[k];
else if (k.startsWith('on') && typeof attrs[k] === 'function') e.addEventListener(k.slice(2), attrs[k]);
else e.setAttribute(k, attrs[k]);
}
if (children) (Array.isArray(children) ? children : [children]).forEach(c => {
if (c == null) return;
e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
});
return e;
}
function debounce(fn, ms) { let t; return function () { clearTimeout(t); t = setTimeout(() => fn.apply(this, arguments), ms); }; }
function toast(msg, kind) {
const t = $('#toast');
t.textContent = msg;
t.className = 'fixed bottom-5 left-1/2 -translate-x-1/2 px-4 py-2 rounded text-white text-sm shadow-lg z-50 ' +
(kind === 'error' ? 'bg-red-600' : kind === 'warn' ? 'bg-amber-600' : 'bg-slate-900');
t.classList.remove('hidden');
setTimeout(() => t.classList.add('hidden'), 2400);
}
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
}
// CrossScriptingFilter 가 엔티티(( 등)로 치환한 값 복원. input value 로만 사용 → 안전, 멱등.
function decodeEntities(s) { if (s == null || s === '') return s; var t = document.createElement('textarea'); t.innerHTML = String(s); return t.value; }
// 잠금 필드 표준 렌더
function lockedInput(value, opts) {
opts = opts || {};
const cls = 'w-full px-3 py-1.5 pr-8 text-sm rounded border border-slate-200 bg-slate-100 text-slate-600 cursor-not-allowed';
return `