API 그룹 팝업 JSP 파일 추가 - 예제 코드 및 UI 구조 작성
Summernote 에디터 스타일 CSS 추가 - 텍스트 및 컴포넌트 스타일 정의 OpenAPI POC JS 파일 추가 - API 스펙 빌더 및 스텝별 렌더 구현
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Editor Content Styles (editor-content.css)
|
||||
*
|
||||
* Summernote 에디터로 작성된 HTML 콘텐츠의 공통 스타일
|
||||
* 관리자포탈/개발자포탈 양쪽에서 동일하게 사용
|
||||
*
|
||||
* 사용법:
|
||||
* - 관리자포탈: Summernote .note-editable에 .editor-content 클래스 추가
|
||||
* - 개발자포탈: 콘텐츠 wrapper에 .editor-content 클래스 추가
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @date 2025-12-30
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
CSS Reset + 기본 설정
|
||||
========================================================================== */
|
||||
|
||||
.editor-content {
|
||||
all: revert;
|
||||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 1.8 !important;
|
||||
color: #1A1A2E !important;
|
||||
word-wrap: break-word;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
헤딩 (Headings)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content h1 {
|
||||
font-size: 20px !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 32px 0 16px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content h1:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.editor-content h2 {
|
||||
font-size: 18px !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 24px 0 8px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content h3 {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 16px 0 8px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content h4,
|
||||
.editor-content h5,
|
||||
.editor-content h6 {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 16px 0 8px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
단락 (Paragraphs)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content p {
|
||||
margin-bottom: 16px !important;
|
||||
line-height: 1.8 !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.editor-content p:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
리스트 (Lists) - 글로벌 reset 대응
|
||||
========================================================================== */
|
||||
|
||||
.editor-content ul {
|
||||
list-style-type: disc !important;
|
||||
padding-left: 32px !important;
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content ol {
|
||||
list-style-type: decimal !important;
|
||||
padding-left: 32px !important;
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content li {
|
||||
margin-bottom: 8px !important;
|
||||
line-height: 1.8 !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.editor-content li:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* 중첩 리스트 */
|
||||
.editor-content ul ul {
|
||||
list-style-type: circle !important;
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content ul ul ul {
|
||||
list-style-type: square !important;
|
||||
}
|
||||
|
||||
.editor-content ol ol {
|
||||
list-style-type: lower-alpha !important;
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content ol ol ol {
|
||||
list-style-type: lower-roman !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
테이블 (Tables)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content table {
|
||||
width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
margin: 24px 0 !important;
|
||||
border: 1px solid #E2E8F0 !important;
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content th,
|
||||
.editor-content td {
|
||||
border: 1px solid #E2E8F0 !important;
|
||||
padding: 8px 16px !important;
|
||||
text-align: left !important;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.editor-content th {
|
||||
background: #EFF6FF !important;
|
||||
font-weight: 600 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content tr:hover {
|
||||
background: #F8FAFC !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
링크 (Links)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content a {
|
||||
color: #0049b4 !important;
|
||||
text-decoration: underline !important;
|
||||
transition: color 0.3s ease !important;
|
||||
}
|
||||
|
||||
.editor-content a:hover {
|
||||
color: #003080 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
인용문 (Blockquote)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content blockquote {
|
||||
border-left: 4px solid #0049b4 !important;
|
||||
padding: 16px 24px !important;
|
||||
margin: 24px 0 !important;
|
||||
background: #F8FAFC !important;
|
||||
font-style: italic !important;
|
||||
color: #64748B !important;
|
||||
border-radius: 0 8px 8px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content blockquote p {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
이미지 (Images)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 8px !important;
|
||||
margin: 16px 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
코드 (Code)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content code {
|
||||
background: #F8FAFC !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 4px !important;
|
||||
font-family: 'Fira Code', 'Courier New', monospace !important;
|
||||
font-size: 0.9em !important;
|
||||
color: #0049b4 !important;
|
||||
}
|
||||
|
||||
.editor-content pre {
|
||||
background: #F8FAFC !important;
|
||||
border: 1px solid #E2E8F0 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 16px !important;
|
||||
overflow-x: auto !important;
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content pre code {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
구분선 (Horizontal Rule)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content hr {
|
||||
border: none !important;
|
||||
border-top: 1px solid #E2E8F0 !important;
|
||||
margin: 32px 0 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
텍스트 강조 (Text Emphasis)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content strong,
|
||||
.editor-content b {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.editor-content em,
|
||||
.editor-content i {
|
||||
font-style: italic !important;
|
||||
}
|
||||
|
||||
.editor-content u {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.editor-content s,
|
||||
.editor-content strike,
|
||||
.editor-content del {
|
||||
text-decoration: line-through !important;
|
||||
}
|
||||
|
||||
.editor-content mark {
|
||||
background-color: #FEF3C7 !important;
|
||||
padding: 0 2px !important;
|
||||
}
|
||||
|
||||
.editor-content sub {
|
||||
vertical-align: sub !important;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
.editor-content sup {
|
||||
vertical-align: super !important;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
반응형 (Responsive)
|
||||
========================================================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.editor-content {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content h1 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.editor-content h2 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.editor-content h3,
|
||||
.editor-content h4,
|
||||
.editor-content h5,
|
||||
.editor-content h6 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content p,
|
||||
.editor-content li {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content table {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.editor-content th,
|
||||
.editor-content td {
|
||||
padding: 4px 8px !important;
|
||||
}
|
||||
|
||||
.editor-content ul,
|
||||
.editor-content ol {
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
|
||||
.editor-content blockquote {
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.editor-content pre {
|
||||
padding: 8px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// 계좌이체 API 모의 데이터
|
||||
// locked: true 인 필드는 게이트웨이가 자동 주입한 값(회색 + 자물쇠)
|
||||
// locked: false / 미지정 인 필드는 사용자가 자유롭게 편집
|
||||
|
||||
window.SAMPLE_DATA = (function () {
|
||||
return {
|
||||
info: {
|
||||
title: { value: '계좌이체 API', locked: false },
|
||||
version: { value: '1.0.0', locked: false },
|
||||
summary: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
termsOfService: { value: '', locked: false },
|
||||
contact: {
|
||||
name: { value: 'DJB API Team', locked: false },
|
||||
email: { value: 'api@djb.co.kr', locked: false },
|
||||
url: { value: '', locked: false }
|
||||
},
|
||||
license: {
|
||||
name: { value: '', locked: false },
|
||||
url: { value: '', locked: false }
|
||||
}
|
||||
},
|
||||
|
||||
tags: [
|
||||
{ name: 'transfer', description: '계좌이체 관련 API' }
|
||||
],
|
||||
|
||||
externalDocs: {
|
||||
description: { value: '', locked: false },
|
||||
url: { value: '', locked: false }
|
||||
},
|
||||
|
||||
servers: [
|
||||
{
|
||||
url: { value: 'https://api-dev.djb.co.kr', locked: true },
|
||||
env: 'development',
|
||||
description: { value: '개발 환경', locked: false }
|
||||
},
|
||||
{
|
||||
url: { value: 'https://api-stg.djb.co.kr', locked: true },
|
||||
env: 'staging',
|
||||
description: { value: '스테이징 환경', locked: false }
|
||||
},
|
||||
{
|
||||
url: { value: 'https://api.djb.co.kr', locked: true },
|
||||
env: 'production',
|
||||
description: { value: '운영 환경', locked: false }
|
||||
}
|
||||
],
|
||||
|
||||
serverVariables: [
|
||||
{
|
||||
name: { value: 'basePath', locked: true },
|
||||
default: { value: '/v1', locked: false },
|
||||
enumStr: { value: '/v1,/v2', locked: false },
|
||||
description: { value: 'API 버전 경로', locked: false }
|
||||
}
|
||||
],
|
||||
|
||||
operation: {
|
||||
method: { value: 'POST', locked: true },
|
||||
path: { value: '/v1/accounts/transfer', locked: true },
|
||||
operationId: { value: 'transferAccount', locked: false },
|
||||
summary: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
tags: { value: ['transfer'], locked: false },
|
||||
deprecated: { value: false, locked: false }
|
||||
},
|
||||
|
||||
// 파라미터 (Path/Query/Header/Cookie)
|
||||
parameters: [
|
||||
{
|
||||
in: 'header',
|
||||
name: { value: 'X-Request-Id', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: 'uuid', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '', locked: false }
|
||||
}
|
||||
],
|
||||
|
||||
// Request Body 스키마 (중첩 3단)
|
||||
requestBody: {
|
||||
mediaType: 'application/json',
|
||||
required: { value: true, locked: true },
|
||||
schema: [
|
||||
{
|
||||
name: { value: 'transactionId', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '40', locked: false },
|
||||
pattern: { value: '^tx-[0-9a-zA-Z-]+$', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: 'tx-20260522-001', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'sender', locked: true },
|
||||
type: { value: 'object', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '송금인 정보', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
children: [
|
||||
{
|
||||
name: { value: 'accountNumber', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '20', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '110-1234-567890', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'bankCode', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '3', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '088', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'holderName', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '60', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '김철수', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { value: 'receiver', locked: true },
|
||||
type: { value: 'object', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '수취인 정보', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
children: [
|
||||
{
|
||||
name: { value: 'accountNumber', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '20', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '111-2345-678901', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'bankCode', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '3', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '020', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'holderName', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '60', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '이영희', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { value: 'amount', locked: true },
|
||||
type: { value: 'object', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '이체 금액', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
children: [
|
||||
{
|
||||
name: { value: 'value', locked: true },
|
||||
type: { value: 'integer', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: 'int64', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '50000', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'currency', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '3', locked: false },
|
||||
pattern: { value: '^[A-Z]{3}$', locked: false },
|
||||
description: { value: 'ISO 4217 통화코드', locked: false },
|
||||
example: { value: 'KRW', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { value: 'memo', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: false, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '100', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '5월 회비', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 응답 스키마 (상태코드 별)
|
||||
responses: {
|
||||
'200': {
|
||||
description: { value: '이체 성공', locked: false },
|
||||
schema: [
|
||||
{ name: { value: 'transactionId', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: 'tx-20260522-001', locked: false }, children: null },
|
||||
{ name: { value: 'status', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: 'COMPLETED', locked: false }, children: null },
|
||||
{ name: { value: 'completedAt', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: 'date-time', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '2026-05-22T10:00:00Z', locked: false }, children: null },
|
||||
{
|
||||
name: { value: 'balance', locked: true }, type: { value: 'object', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '잔액 정보', locked: false }, example: { value: '', locked: false },
|
||||
children: [
|
||||
{ name: { value: 'before', locked: true }, type: { value: 'integer', locked: true }, required: { value: true, locked: true }, format: { value: 'int64', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '1000000', locked: false }, children: null },
|
||||
{ name: { value: 'after', locked: true }, type: { value: 'integer', locked: true }, required: { value: true, locked: true }, format: { value: 'int64', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '950000', locked: false }, children: null }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'400': {
|
||||
description: { value: '잘못된 요청', locked: false },
|
||||
schema: [
|
||||
{
|
||||
name: { value: 'error', locked: true }, type: { value: 'object', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '', locked: false },
|
||||
children: [
|
||||
{ name: { value: 'code', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: 'INVALID_AMOUNT', locked: false }, children: null },
|
||||
{ name: { value: 'message', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '금액이 올바르지 않습니다', locked: false }, children: null },
|
||||
{ name: { value: 'details', locked: true }, type: { value: 'array', locked: true }, required: { value: false, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '', locked: false }, itemsType: { value: 'string', locked: false }, children: null }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'401': { description: { value: '인증 실패', locked: false }, schema: [] },
|
||||
'500': { description: { value: '서버 오류', locked: false }, schema: [] }
|
||||
},
|
||||
|
||||
// 보안 스킴
|
||||
securitySchemes: [
|
||||
{
|
||||
key: 'ApiKeyAuth',
|
||||
type: 'apiKey',
|
||||
locked: true,
|
||||
name: { value: 'X-API-Key', locked: true },
|
||||
in: { value: 'header', locked: true },
|
||||
description: { value: '', locked: false }
|
||||
}
|
||||
],
|
||||
globalSecurity: ['ApiKeyAuth'],
|
||||
|
||||
// 예제
|
||||
examples: {
|
||||
request: {
|
||||
'application/json': {
|
||||
default: '{\n "transactionId": "tx-20260522-001",\n "sender": {\n "accountNumber": "110-1234-567890",\n "bankCode": "088",\n "holderName": "김철수"\n },\n "receiver": {\n "accountNumber": "111-2345-678901",\n "bankCode": "020",\n "holderName": "이영희"\n },\n "amount": {\n "value": 50000,\n "currency": "KRW"\n },\n "memo": "5월 회비"\n}'
|
||||
}
|
||||
},
|
||||
response: {
|
||||
'200': { body: '{\n "transactionId": "tx-20260522-001",\n "status": "COMPLETED"\n}', headers: [ { name: 'X-RateLimit-Remaining', type: 'integer', description: '남은 요청 횟수', example: '99' } ] }
|
||||
}
|
||||
},
|
||||
|
||||
// 문서 옵션
|
||||
docOptions: {
|
||||
theme: 'light',
|
||||
lang: 'ko',
|
||||
includeExamples: true,
|
||||
tryItOut: true,
|
||||
defaultExpandDepth: -1,
|
||||
layout: 'BaseLayout',
|
||||
tagFilter: [],
|
||||
sideToc: true
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,103 @@
|
||||
/* DJB OpenAPI Editor POC — Tailwind 보강 커스텀 */
|
||||
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo",
|
||||
"Malgun Gothic", "Noto Sans KR", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* === 잠금 행/카드 좌측 띠 === */
|
||||
.locked-row td:first-child {
|
||||
position: relative;
|
||||
}
|
||||
.locked-row td:first-child::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
width: 3px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.locked-card {
|
||||
position: relative;
|
||||
background: linear-gradient(to right, #f8fafc 0%, #ffffff 4px) #ffffff;
|
||||
}
|
||||
.locked-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; top: 8px; bottom: 8px;
|
||||
width: 3px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* === 스키마 테이블 — 가독성 보조 === */
|
||||
.schema-table th { white-space: nowrap; }
|
||||
.schema-table tbody tr:hover { background: #fafbfc; }
|
||||
.schema-table input,
|
||||
.schema-table select {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.schema-table input[type="text"] {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
/* === 스텝퍼 항목 hover === */
|
||||
.step-item:hover { background: #f8fafc; }
|
||||
|
||||
/* === 미리보기 패널: Swagger UI 크기 미세조정 === */
|
||||
#preview-swagger .swagger-ui {
|
||||
font-size: 13px;
|
||||
}
|
||||
#preview-swagger .swagger-ui .info { margin: 18px 0; }
|
||||
#preview-swagger .swagger-ui .info .title { font-size: 22px; }
|
||||
#preview-swagger .swagger-ui .scheme-container { padding: 12px 0; box-shadow: none; }
|
||||
#preview-swagger .swagger-ui .opblock { margin: 0 0 12px 0; }
|
||||
|
||||
/* === 다크 테마 (POC 시뮬레이션 — Swagger 자체 다크는 미지원이라 컨테이너만) === */
|
||||
body.theme-dark #preview-swagger { background: #1e293b; }
|
||||
|
||||
/* === 토스트 애니메이션 === */
|
||||
#toast { transition: opacity 0.2s; }
|
||||
|
||||
/* === Monaco 컨테이너가 hidden 일 때 layout 깨짐 방지 === */
|
||||
#preview-monaco {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* === Stepper 6단 grid 셀 좌우 라인 (마지막 셀 제외) === */
|
||||
#stepper li {
|
||||
position: relative;
|
||||
}
|
||||
#stepper li:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
/* === 보안 스킴 카드 — 잠금 표시 === */
|
||||
.locked-card {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
/* === 칩(태그) 미세 조정 === */
|
||||
.tag-chip { transition: background 0.15s; }
|
||||
|
||||
/* === Export menu fade === */
|
||||
#export-menu { animation: fadeIn 0.12s ease-out; }
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* === 1600px 기준 폼 패널 내부 가로 여유 확보 === */
|
||||
#form-content { max-width: 100%; }
|
||||
|
||||
/* === readonly input 위에서 텍스트 커서 안 보이게 === */
|
||||
input[readonly] { user-select: text; }
|
||||
@@ -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>
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
var url ='<c:url value="/onl/transaction/apim/apiInterfaceMan.json" />';
|
||||
const url_excel ='<c:url value="/onl/transaction/apim/apiInterfaceMan.excel" />';
|
||||
var url_spec = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
|
||||
var url_openapi_spec = '<c:url value="/onl/transaction/apim/djbApiSpecMan.view"/>';
|
||||
var returnUrl ;
|
||||
let headerValues = [];
|
||||
let headerNames = [];
|
||||
@@ -993,6 +994,28 @@
|
||||
showModal(popupUrl, args, 1200, 800);
|
||||
}
|
||||
|
||||
// v2: OpenAPI Spec — 기본은 모달(iframe), Shift+클릭은 새 창.
|
||||
// 컨텍스트는 전부 query string 으로 전달(dialogArguments 미사용).
|
||||
function showOpenApiSpecPopup(e) {
|
||||
var eaiSvcName = getFullSvcName();
|
||||
var eaiSvcDesc = $('#eaiSvcDesc').val();
|
||||
var apipath = $('#apiFullPath').val(); // METHOD|URL ('|' 포함 → 인코딩 필수)
|
||||
var contentType = $('#contentType').val();
|
||||
var baseUrl = url_openapi_spec
|
||||
+ '?cmd=DETAIL'
|
||||
+ '&eaiSvcName=' + encodeURIComponent(eaiSvcName)
|
||||
+ '&apipath=' + encodeURIComponent(apipath)
|
||||
+ '&eaiSvcDesc=' + encodeURIComponent(eaiSvcDesc)
|
||||
+ '&contentType=' + encodeURIComponent(contentType);
|
||||
if (e && e.shiftKey) {
|
||||
// Shift+클릭: 새 창. window.open 은 serviceType/menuId 자동 부착 안 되므로 직접 붙임.
|
||||
window.open(urlAddServiceType(baseUrl + '&menuId=' + encodeURIComponent(getMenuId())), '_blank');
|
||||
} else {
|
||||
// 기본: 모달 + iframe. showModal 이 serviceType/menuId/pop 을 자동 부착.
|
||||
showModal(baseUrl, { title: 'OpenAPI Spec' }, 1600, 880);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
makeValidate();
|
||||
// var bootstrapButton = $.fn.button.noConflict()
|
||||
@@ -1007,9 +1030,14 @@
|
||||
}
|
||||
init(url,key,detail);
|
||||
|
||||
// v1: 기존 API 스펙 (모달) — 기능 비교용 복원
|
||||
$("#btn_api_spec").click(function() {
|
||||
showApiSpecPopup();
|
||||
});
|
||||
// v2: 신규 OpenAPI Spec (새 탭)
|
||||
$("#btn_openapi_spec").click(function(e) {
|
||||
showOpenApiSpecPopup(e);
|
||||
});
|
||||
|
||||
$("#btn_modify").click(modifyApi);
|
||||
$("#btn_delete").click(function(){
|
||||
@@ -1508,7 +1536,10 @@
|
||||
<button type="button" class="cssbtn" id="btn_json_export" level="R" status="DETAIL,NEW"><i class="material-icons">download</i> <%=localeMessage.getString("button.exportJson")%></button></button>
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="R" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
|
||||
<!-- v1: 기존 API 스펙 (모달) — 기능 비교용 복원 -->
|
||||
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL,NEW"><i class="material-icons">description</i> API 스펙</button>
|
||||
<!-- v2: 신규 OpenAPI Spec (새 탭) -->
|
||||
<button type="button" class="cssbtn" id="btn_openapi_spec" level="R" status="DETAIL,NEW"><i class="material-icons">api</i> OpenAPI Spec</button>
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title" id="title" style="font-size:1.4em">${rmsMenuName}</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/transaction/apim/apiSpecMan.json" />';
|
||||
var url_org ='<c:url value="/onl/transaction/apim/apiSpecManPopup.view" />';
|
||||
var url_org ='<c:url value="/onl/transaction/apim/apiSpecMan.view" />';
|
||||
var isDetail = false;
|
||||
var sampleRequestEditor, sampleResponseEditor, testbedSpecEditor;
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<%@ page import="java.io.*"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title>API 그룹 선택</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css_custom.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/apigroup/apiGroupMan.json" />';
|
||||
|
||||
function search() {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var selectedGroupIds = urlParams.get("selectedGroupIds");
|
||||
|
||||
if (selectedGroupIds) {
|
||||
selectedGroupIds = selectedGroupIds.split(','); // 콤마로 구분된 ID들을 배열로 변환
|
||||
} else {
|
||||
selectedGroupIds = [];
|
||||
}
|
||||
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['API 그룹 ID', 'API 그룹 명', '설명'],
|
||||
colModel: [
|
||||
{name: 'id', hidden: true},
|
||||
{name: 'groupName', align: 'center', width: "180"},
|
||||
{name: 'groupDesc', align: 'left', width: "200"}
|
||||
],
|
||||
jsonReader: {
|
||||
repeatitems: false
|
||||
},
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: 'auto',
|
||||
width: 410,
|
||||
autowidth: false,
|
||||
viewrecords: true,
|
||||
shrinkToFit: false,
|
||||
multiselect: true,
|
||||
multiboxonly: false,
|
||||
loadComplete: function(data) {
|
||||
var $grid = $(this);
|
||||
$.each($grid.getDataIDs(), function(_, id) {
|
||||
var rowData = $grid.getRowData(id);
|
||||
if ($.inArray(rowData.id, selectedGroupIds) !== -1) {
|
||||
$grid.jqGrid('setSelection', id, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_search").click(search);
|
||||
|
||||
$("#btn_save").click(function() {
|
||||
var selectedRows = $('#grid').getGridParam('selarrrow');
|
||||
|
||||
var selectedGroups = [];
|
||||
if (selectedRows.length > 0) {
|
||||
selectedGroups = selectedRows.map(function(rowid) {
|
||||
var rowData = $('#grid').getRowData(rowid);
|
||||
return {
|
||||
id: rowData.id,
|
||||
groupName: rowData.groupName
|
||||
};
|
||||
});
|
||||
}
|
||||
// 부모 창의 selectApiGroup 함수 호출
|
||||
if (window.parent && typeof window.parent.selectApiGroup === "function") {
|
||||
window.parent.selectApiGroup(selectedGroups);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_close").click(function () {
|
||||
window.close();
|
||||
});
|
||||
|
||||
// 검색어 입력 후 엔터 키 처리
|
||||
$("input[name=searchGroupName]").keydown(function(key) {
|
||||
if (key.keyCode == 13) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup_box">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_save" level="W">
|
||||
<i class="material-icons">save</i> <%= localeMessage.getString("button.save") %>
|
||||
</button>
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R">
|
||||
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
|
||||
</button>
|
||||
<button type="button" class="cssbtn" id="btn_close" level="R" status="DETAIL"><i
|
||||
class="material-icons">close</i> <%= localeMessage.getString("button.close") %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="title"></div>
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<tr>
|
||||
<th style="width:80px;">API 그룹 명</th>
|
||||
<td>
|
||||
<input type="text" name="searchGroupName" value="${param.searchGroupName}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,173 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1600" />
|
||||
<title>OpenAPI Spec 에디터</title>
|
||||
|
||||
<%-- Tailwind (벤더된 Play CDN JS, 오프라인) --%>
|
||||
<script src="<c:url value='/plugins/openapi/tailwind.js'/>"></script>
|
||||
<script>
|
||||
tailwind.config = { theme: { extend: { colors: { brand: {
|
||||
50:'#eff6ff', 100:'#dbeafe', 500:'#3b82f6', 600:'#2563eb', 700:'#1d4ed8'
|
||||
} } } } };
|
||||
</script>
|
||||
|
||||
<%-- Swagger UI 5 (벤더) --%>
|
||||
<link rel="stylesheet" href="<c:url value='/plugins/swaggerUI/swagger-ui.css'/>" />
|
||||
<script src="<c:url value='/plugins/swaggerUI/swagger-ui-bundle.js'/>"></script>
|
||||
<script src="<c:url value='/plugins/swaggerUI/swagger-ui-standalone-preset.js'/>"></script>
|
||||
<script src="<c:url value='/plugins/swaggerUI/djb-swagger-i18n.js'/>" charset="UTF-8"></script>
|
||||
|
||||
<%-- js-yaml (벤더) --%>
|
||||
<script src="<c:url value='/plugins/openapi/js-yaml.min.js'/>"></script>
|
||||
|
||||
<%-- CodeMirror (YAML 편집기 — Monaco 대체, admin addon) --%>
|
||||
<link rel="stylesheet" href="<c:url value='/addon/codemirror/lib/codemirror.css'/>" />
|
||||
<script src="<c:url value='/addon/codemirror/lib/codemirror.js'/>"></script>
|
||||
<script src="<c:url value='/addon/codemirror/mode/yaml/yaml.js'/>"></script>
|
||||
<script src="<c:url value='/addon/codemirror/mode/javascript/javascript.js'/>"></script>
|
||||
|
||||
<%-- jQuery + Summernote(lite, 무-Bootstrap) — 설명 리치 에디터 --%>
|
||||
<link rel="stylesheet" href="<c:url value='/addon/summernote/summernote-lite.css'/>" />
|
||||
<%-- 개발자포탈과 동일한 에디터 콘텐츠 스타일(.editor-content, Noto Sans KR 16px) --%>
|
||||
<link rel="stylesheet" href="<c:url value='/js/djb/apispec/editor-content.css'/>" />
|
||||
<script src="<c:url value='/js/jquery-1.12.1.min.js'/>"></script>
|
||||
<script src="<c:url value='/addon/summernote/summernote-lite.min.js'/>"></script>
|
||||
<script src="<c:url value='/addon/summernote/lang/summernote-ko-KR.js'/>"></script>
|
||||
|
||||
<link rel="stylesheet" href="<c:url value='/js/djb/apispec/styles.css'/>" />
|
||||
<style>
|
||||
/* 페이지 자체 세로 스크롤 제거 — 뷰포트 고정, 내부 패널만 스크롤 */
|
||||
html, body { height: 100%; margin: 0; overflow-x: auto; overflow-y: hidden; }
|
||||
#djb-wrap { height: 100vh; display: flex; flex-direction: column; }
|
||||
#djb-wrap > header, #djb-wrap > nav { flex: 0 0 auto; }
|
||||
#body { flex: 1 1 auto; min-height: 0 !important; overflow: hidden; }
|
||||
#form-panel { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||
#form-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
||||
#preview-panel { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||
#preview-swagger, #preview-monaco { flex: 1 1 auto; min-height: 0; height: auto !important; overflow: auto; }
|
||||
#preview-monaco .CodeMirror { height: 100%; }
|
||||
/* 패널 내부 sticky 는 flex 행으로 (스크롤 컨테이너는 form-content / preview) */
|
||||
#form-panel > .sticky, #preview-panel > .sticky { position: static !important; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.DJB_CTX = {
|
||||
eaiSvcName: "${param.eaiSvcName}",
|
||||
apiName: "${param.eaiSvcDesc}",
|
||||
detailUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>?cmd=DETAIL_SPEC&serviceType=APIGW",
|
||||
saveUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>?cmd=SAVE&serviceType=APIGW",
|
||||
swaggerBase: "<c:url value='/plugins/swaggerUI/'/>",
|
||||
gwAddress: "${gwAddress}",
|
||||
orgPopupUrl: "<c:url value='/onl/transaction/apim/apiSpecMan.view'/>",
|
||||
groupPopupUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.view'/>?cmd=GROUP_POPUP",
|
||||
jsonUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>"
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800 antialiased" style="min-width:1600px;">
|
||||
|
||||
<div id="djb-wrap" class="mx-auto" style="width:1600px;">
|
||||
|
||||
<!-- ===== Header ===== -->
|
||||
<header class="h-14 flex items-center justify-between px-6 bg-white border-b border-slate-200 sticky top-0 z-30">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="font-semibold text-slate-900">OpenAPI Spec 에디터</div>
|
||||
<span class="text-slate-300">|</span>
|
||||
<span class="text-sm text-slate-500">인터페이스:</span>
|
||||
<span class="text-sm font-medium text-slate-800" id="hdr-project-name">${param.eaiSvcName}</span>
|
||||
<span class="ml-2 px-2 py-0.5 text-[11px] rounded-full bg-slate-100 text-slate-600 border border-slate-200" id="hdr-version">v1.0.0</span>
|
||||
<span id="hdr-source" class="hidden ml-1 px-2 py-0.5 text-[11px] rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="btn-newwin" class="hidden px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">새창으로 띄우기</button>
|
||||
<button id="btn-regen" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">자동 생성(초기화)</button>
|
||||
<button id="btn-save" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700">저장</button>
|
||||
<div class="relative">
|
||||
<button id="btn-export" class="px-3 py-1.5 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">내보내기 ▾</button>
|
||||
<div id="export-menu" class="hidden absolute right-0 mt-1 w-44 bg-white border border-slate-200 rounded shadow-lg z-40">
|
||||
<button data-export="yaml" class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-50">YAML 다운로드</button>
|
||||
<button data-export="json" class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-50">JSON 다운로드</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-close" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">닫기</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ===== Stepper ===== -->
|
||||
<nav class="bg-white border-b border-slate-200 sticky z-20" style="top:56px;">
|
||||
<div class="h-1 bg-slate-100">
|
||||
<div id="stepper-progress" class="h-1 bg-brand-500 transition-all duration-300" style="width:16.66%"></div>
|
||||
</div>
|
||||
<ol id="stepper" class="grid grid-cols-5 px-2"></ol>
|
||||
</nav>
|
||||
|
||||
<!-- ===== Body ===== -->
|
||||
<main id="body" class="flex" style="min-height: calc(100vh - 56px - 72px);">
|
||||
<section id="form-panel" class="bg-white border-r border-slate-200 transition-all duration-300" style="width:960px;">
|
||||
<div class="flex items-center justify-between px-6 py-3 border-b border-slate-100 sticky bg-white z-10" style="top:128px;">
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="text-xs uppercase tracking-wide text-slate-400">Step <span id="form-step-num">1</span> / 5</span>
|
||||
<h2 class="text-base font-semibold text-slate-800" id="form-step-title">기본정보</h2>
|
||||
</div>
|
||||
<button id="btn-toggle-preview-from-form" class="hidden text-xs px-2 py-1 rounded border border-slate-200 text-slate-600 hover:bg-slate-50">▶ 미리보기 다시 열기</button>
|
||||
</div>
|
||||
<div id="form-content" class="px-6 py-5"></div>
|
||||
<div class="sticky bottom-0 bg-white border-t border-slate-200 px-6 py-3 flex items-center justify-between">
|
||||
<button id="btn-prev" class="px-4 py-2 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed">◀ 이전</button>
|
||||
<div class="text-xs text-slate-500" id="footer-hint">필수 항목을 채우고 다음 단계로 이동하세요</div>
|
||||
<button id="btn-next" class="px-4 py-2 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">다음 ▶</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside id="preview-panel" class="bg-slate-50 transition-all duration-300" style="width:640px;">
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b border-slate-200 bg-white sticky z-10" style="top:128px;">
|
||||
<div class="flex items-center gap-1">
|
||||
<button data-ptab="swagger" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-brand-600 text-brand-700 font-medium">Swagger UI</button>
|
||||
<button data-ptab="yaml-ro" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-transparent text-slate-600 hover:text-slate-800">YAML 보기</button>
|
||||
<button data-ptab="yaml-edit" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-transparent text-slate-600 hover:text-slate-800">스펙 편집</button>
|
||||
</div>
|
||||
<button id="btn-toggle-preview" class="text-xs px-2 py-1 rounded border border-slate-200 text-slate-600 hover:bg-slate-100">패널 숨기기 ▶</button>
|
||||
</div>
|
||||
<div id="yaml-edit-bar" class="hidden flex items-center justify-between bg-amber-50 border-b border-amber-200 px-4 py-2 text-xs">
|
||||
<span class="text-amber-800">YAML 을 직접 편집한 뒤 [Form 에 적용] 을 눌러 양식에 동기화하세요.</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="btn-yaml-revert" class="px-2 py-1 rounded border border-amber-300 bg-white hover:bg-amber-100">변경 취소</button>
|
||||
<button id="btn-yaml-apply" class="px-2 py-1 rounded bg-amber-600 text-white hover:bg-amber-700">Form 에 적용</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="preview-swagger" class="overflow-auto" style="height: calc(100vh - 56px - 72px - 41px);"></div>
|
||||
<div id="preview-monaco" class="hidden" style="height: calc(100vh - 56px - 72px - 41px);"></div>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="fixed bottom-5 left-1/2 -translate-x-1/2 hidden px-4 py-2 rounded bg-slate-900 text-white text-sm shadow-lg z-50"></div>
|
||||
|
||||
<!-- Security scheme modal -->
|
||||
<div id="sec-modal" class="hidden fixed inset-0 z-50 bg-slate-900/40 flex items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-xl w-[640px] max-h-[80vh] overflow-auto">
|
||||
<div class="flex items-center justify-between px-5 py-3 border-b border-slate-200">
|
||||
<h3 class="font-semibold text-slate-800">보안 스킴 추가</h3>
|
||||
<button class="text-slate-400 hover:text-slate-700" data-sec-close>✕</button>
|
||||
</div>
|
||||
<div class="p-5" id="sec-modal-body"></div>
|
||||
<div class="px-5 py-3 border-t border-slate-200 flex justify-end gap-2 bg-slate-50">
|
||||
<button class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white" data-sec-close>취소</button>
|
||||
<button id="btn-sec-save" class="px-3 py-1.5 text-sm rounded bg-brand-600 text-white">추가</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<c:url value='/js/djb/apispec/sample-data.js'/>"></script>
|
||||
<script src="<c:url value='/js/djb/apispec/app.js'/>"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,122 @@
|
||||
/*!
|
||||
* djb-swagger-i18n.js
|
||||
* Swagger UI 영문 라벨을 한글로 치환하는 i18n 패치 (방법 B - DOM patch)
|
||||
* 배포 경로 권장: static/plugins/swaggerUI/djb-swagger-i18n.js
|
||||
* index.html 의 swagger-initializer.js 직전 또는 직후에 <script> 로 로드
|
||||
*
|
||||
* ?lang=en 쿼리스트링이 있으면 패치 비활성화 (원문 보기)
|
||||
* 매핑 카탈로그는 01-i18n-mapping/i18n-strings.csv 와 동기 유지
|
||||
*/
|
||||
(function (window) {
|
||||
'use strict';
|
||||
|
||||
// ── 비활성화 옵션 ──
|
||||
if (/[?&]lang=en\b/.test(window.location.search)) return;
|
||||
|
||||
// ── 매핑 (key 는 디버깅용, 실제 비교는 EN 문자열 일치) ──
|
||||
var I18N_MAP = {
|
||||
// P0 — OAuth2 모달 본문
|
||||
'auth.modal.title': { en: 'Available authorizations', ko: '사용 가능한 인증' },
|
||||
'auth.scopes.description': {
|
||||
en: 'Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.',
|
||||
ko: '스코프는 최종 사용자를 대신하여 애플리케이션이 데이터에 접근할 수 있는 권한 수준을 부여합니다. 각 API는 하나 이상의 스코프를 선언할 수 있습니다.'
|
||||
},
|
||||
'auth.scopes.swagger_grant': {
|
||||
en: 'API requires the following scopes. Select which ones you want to grant to Swagger UI.',
|
||||
ko: '이 API 는 아래 스코프가 필요합니다. Swagger UI 에 부여할 스코프를 선택하세요.'
|
||||
},
|
||||
'auth.flow.clientCredentials': { en: 'OAuth2 client credentials flow', ko: 'OAuth2 클라이언트 자격증명 흐름' },
|
||||
'auth.flow.implicit': { en: 'OAuth2 implicit flow', ko: 'OAuth2 암묵적 흐름' },
|
||||
'auth.flow.password': { en: 'OAuth2 password flow', ko: 'OAuth2 비밀번호 흐름' },
|
||||
'auth.flow.authorizationCode': { en: 'OAuth2 authorization code flow', ko: 'OAuth2 인가 코드 흐름' },
|
||||
'auth.field.tokenUrl': { en: 'Token URL:', ko: '토큰 URL:' },
|
||||
'auth.field.flow': { en: 'Flow:', ko: '흐름:' },
|
||||
'auth.field.clientId': { en: 'client_id:', ko: '클라이언트 ID (client_id):' },
|
||||
'auth.field.clientSecret': { en: 'client_secret:',ko: '클라이언트 시크릿 (client_secret):' },
|
||||
'auth.field.scopes': { en: 'Scopes:', ko: '스코프:' },
|
||||
'auth.action.selectAll': { en: 'select all', ko: '모두 선택' },
|
||||
'auth.action.selectNone': { en: 'select none', ko: '선택 해제' },
|
||||
'auth.action.authorize': { en: 'Authorize', ko: '인증' },
|
||||
'auth.action.close': { en: 'Close', ko: '닫기' },
|
||||
'auth.action.logout': { en: 'Logout', ko: '로그아웃' },
|
||||
'auth.modal.authorized': { en: 'authorized', ko: '인증됨' },
|
||||
|
||||
// P1 — 기타 인증 흐름
|
||||
'auth.scheme.apiKey': { en: 'API key', ko: 'API 키' },
|
||||
'auth.field.name': { en: 'name:', ko: '이름:' },
|
||||
'auth.field.in': { en: 'in:', ko: '위치:' },
|
||||
'auth.field.value': { en: 'Value:', ko: '값:' },
|
||||
'basic.field.username': { en: 'Username:',ko: '사용자 ID:' },
|
||||
'basic.field.password': { en: 'Password:',ko: '비밀번호:' }
|
||||
// P2 (오퍼레이션/응답) 는 필요 시 추가
|
||||
};
|
||||
|
||||
// 빠른 lookup 을 위해 EN → KO 인덱스 사전 생성
|
||||
var EN_TO_KO = Object.create(null);
|
||||
Object.keys(I18N_MAP).forEach(function (k) {
|
||||
var e = I18N_MAP[k];
|
||||
EN_TO_KO[e.en] = e.ko;
|
||||
});
|
||||
|
||||
// ── 치환 대상 노드 식별 + 치환 ──
|
||||
function translateNode(node) {
|
||||
if (!node) return;
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
var raw = node.nodeValue;
|
||||
if (!raw) return;
|
||||
var trimmed = raw.trim();
|
||||
if (!trimmed) return;
|
||||
// 1) 완전 일치 (가장 안전)
|
||||
if (EN_TO_KO[trimmed]) {
|
||||
node.nodeValue = raw.replace(trimmed, EN_TO_KO[trimmed]);
|
||||
return;
|
||||
}
|
||||
// 2) 라벨 colon 패턴: "Token URL:" 같이 끝이 ":" 인 짧은 라벨
|
||||
// (불필요 — 1번 완전 일치로 대부분 커버)
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE') return;
|
||||
|
||||
// input placeholder / button title 도 일부 케이스에 대비
|
||||
if (node.tagName === 'INPUT' && node.placeholder && EN_TO_KO[node.placeholder.trim()]) {
|
||||
node.placeholder = EN_TO_KO[node.placeholder.trim()];
|
||||
}
|
||||
|
||||
var i, child = node.childNodes, len = child.length;
|
||||
for (i = 0; i < len; i++) translateNode(child[i]);
|
||||
}
|
||||
|
||||
function translateAll() {
|
||||
var root = document.getElementById('swagger-ui') || document.body;
|
||||
translateNode(root);
|
||||
}
|
||||
|
||||
// ── MutationObserver — SwaggerUI 가 동적으로 DOM 을 다시 그릴 때마다 재적용 ──
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
for (var i = 0; i < mutations.length; i++) {
|
||||
var m = mutations[i];
|
||||
if (m.addedNodes && m.addedNodes.length) {
|
||||
for (var j = 0; j < m.addedNodes.length; j++) translateNode(m.addedNodes[j]);
|
||||
}
|
||||
if (m.type === 'characterData') {
|
||||
translateNode(m.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function start() {
|
||||
translateAll();
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', start);
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
})(window);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user