swagger-ui.js.map 파일 업데이트:
- 최신 버전으로 맵 파일 교체 - Swagger UI 동작 관련 불일치 문제 수정
This commit is contained in:
+6
-3
@@ -30,9 +30,12 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'com.eactive.elink.common:elink-common-data:4.5.5'
|
implementation 'com.eactive.elink.common:elink-common-data:4.5.5'
|
||||||
|
|
||||||
// implementation 'io.swagger.core.v3:swagger-core:2.2.25'
|
// OpenAPI 3.0/3.1 spec 지원 (Nexus/Maven 해석). 기존 libs/swagger fileTree 대체.
|
||||||
// implementation 'io.swagger.parser.v3:swagger-parser:2.1.23'
|
// swagger 최신 릴리스(2.2.52/2.1.45)도 SpecVersion 은 V30/V31 까지만 — OpenAPI 3.2 는
|
||||||
implementation fileTree(dir: 'libs/swagger', include: ['*.jar'])
|
// 아직 swagger-core/parser 어느 버전도 미지원. 지원 추가 시 버전만 상향하면 됨. JDK8 호환.
|
||||||
|
implementation 'io.swagger.core.v3:swagger-core:2.2.52' // io.swagger.v3.oas.models.*, io.swagger.v3.core.util.Json
|
||||||
|
implementation 'io.swagger.parser.v3:swagger-parser:2.1.45' // io.swagger.parser.OpenAPIParser, io.swagger.v3.parser.*
|
||||||
|
implementation 'io.swagger:swagger-annotations:1.6.14' // io.swagger.annotations.ApiModelProperty (CommissionSearch)
|
||||||
|
|
||||||
implementation project(':elink-online-core-jpa')
|
implementation project(':elink-online-core-jpa')
|
||||||
implementation project(':elink-portal-common')
|
implementation project(':elink-portal-common')
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,6 +4,8 @@ package com.eactive.apim.portal.apps.apis.filter;
|
|||||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
||||||
import com.eactive.apim.portal.common.util.ApplicationContextUtil;
|
import com.eactive.apim.portal.common.util.ApplicationContextUtil;
|
||||||
|
import com.eactive.apim.portal.djb.testbed.config.DjbTestbedGatewayProperty;
|
||||||
|
import com.eactive.apim.portal.djb.testbed.enums.DjbGatewayMode;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@@ -65,7 +67,13 @@ public class ApiTesterFilter implements Filter {
|
|||||||
ApiService apiSpecInfoDtoService = ApplicationContextUtil.getContext().getBean(ApiService.class);
|
ApiService apiSpecInfoDtoService = ApplicationContextUtil.getContext().getBean(ApiService.class);
|
||||||
String url = httpServletRequest.getHeader("original-url");
|
String url = httpServletRequest.getHeader("original-url");
|
||||||
|
|
||||||
if (url.contains("/api/v1/oauth/token")){
|
// 게이트웨이 모드에 따라 OAuth 토큰 발급 요청을 mock 또는 실 게이트웨이 forward 로 분기 (DJPGPT0001)
|
||||||
|
DjbTestbedGatewayProperty gatewayProperty = ApplicationContextUtil.getContext().getBean(DjbTestbedGatewayProperty.class);
|
||||||
|
DjbGatewayMode gatewayMode = gatewayProperty.resolveGatewayMode();
|
||||||
|
boolean tokenRequest = url.contains(DjbTestbedGatewayProperty.PORTAL_MOCK_TOKEN_PATH)
|
||||||
|
|| url.contains(gatewayProperty.tokenPath());
|
||||||
|
|
||||||
|
if (tokenRequest) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
BufferedReader reader = httpServletRequest.getReader();
|
BufferedReader reader = httpServletRequest.getReader();
|
||||||
String line;
|
String line;
|
||||||
@@ -74,27 +82,40 @@ public class ApiTesterFilter implements Filter {
|
|||||||
}
|
}
|
||||||
String body = sb.toString();
|
String body = sb.toString();
|
||||||
|
|
||||||
// Parse request parameters
|
if (gatewayMode == DjbGatewayMode.PORTAL_MOCK) {
|
||||||
Map<String, String> params = new HashMap<>();
|
// PortalMock: 고정 mock 토큰 반환 (기존 동작 유지)
|
||||||
String[] pairs = body.split("&");
|
Map<String, String> params = new HashMap<>();
|
||||||
for (String pair : pairs) {
|
String[] pairs = body.split("&");
|
||||||
String[] keyValue = pair.split("=");
|
for (String pair : pairs) {
|
||||||
if (keyValue.length == 2) {
|
String[] keyValue = pair.split("=");
|
||||||
params.put(keyValue[0], keyValue[1]);
|
if (keyValue.length == 2) {
|
||||||
|
params.put(keyValue[0], keyValue[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
String scope = params.getOrDefault("scope", "default");
|
||||||
|
|
||||||
|
String token = "{\n" +
|
||||||
|
" \"access_token\": \"djbank_gw_sample_token\",\n" +
|
||||||
|
" \"token_type\": \"bearer\",\n" +
|
||||||
|
" \"expires_in\": 86400,\n" +
|
||||||
|
" \"scope\": \""+scope +"\",\n" +
|
||||||
|
" \"jti\": \""+ UUID.randomUUID().toString()+"\"\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.getWriter().println(token);
|
||||||
|
} else {
|
||||||
|
// GATEWAY: 실 게이트웨이 토큰 엔드포인트로 forward (token 발급만)
|
||||||
|
APISender apiSender = ApplicationContextUtil.getContext().getBean(APISender.class);
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
headers.put("Accept", "application/json");
|
||||||
|
String target = gatewayProperty.baseUrl() + gatewayProperty.tokenPath();
|
||||||
|
String tokenResponse = apiSender.requestPost(target, headers, new HashMap<>(), body);
|
||||||
|
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.getWriter().println(tokenResponse);
|
||||||
}
|
}
|
||||||
String scope = params.getOrDefault("scope", "default");
|
|
||||||
|
|
||||||
String token = "{\n" +
|
|
||||||
" \"access_token\": \"djbank_gw_sample_token\",\n" +
|
|
||||||
" \"token_type\": \"bearer\",\n" +
|
|
||||||
" \"expires_in\": 86400,\n" +
|
|
||||||
" \"scope\": \""+scope +"\",\n" +
|
|
||||||
" \"jti\": \""+ UUID.randomUUID().toString()+"\"\n" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
response.setContentType("application/json");
|
|
||||||
response.getWriter().println(token);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ApiSpecInfoDto apiSpecInfoDto = apiSpecInfoDtoService.selectDetailByURLAndMethod(parseUri(url), httpServletRequest.getMethod());
|
ApiSpecInfoDto apiSpecInfoDto = apiSpecInfoDtoService.selectDetailByURLAndMethod(parseUri(url), httpServletRequest.getMethod());
|
||||||
|
|||||||
@@ -9451,6 +9451,117 @@ button.djb-comment-submit:disabled {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.testbed-app-panel {
|
||||||
|
margin: 8px 0 24px;
|
||||||
|
padding: 24px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid #E2E8F0;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.testbed-app-panel .testbed-app-panel__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.testbed-app-panel .testbed-app-panel__title {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1A1A2E;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.testbed-app-panel .testbed-app-panel__title::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 15px;
|
||||||
|
background: #0049b4;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.testbed-app-panel .testbed-app-panel__desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748B;
|
||||||
|
}
|
||||||
|
.testbed-app-panel .testbed-app-field {
|
||||||
|
position: relative;
|
||||||
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
.testbed-app-panel .testbed-app-field::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 50%;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
margin-top: -6px;
|
||||||
|
border-right: 2px solid #64748B;
|
||||||
|
border-bottom: 2px solid #64748B;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.testbed-app-panel #apps {
|
||||||
|
width: 100%;
|
||||||
|
height: 46px;
|
||||||
|
padding: 0 42px 0 16px;
|
||||||
|
font-family: "Spoqa Han Sans Neo", "SpoqaHanSans", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1A1A2E;
|
||||||
|
background: #F8FAFC;
|
||||||
|
border: 1px solid #E2E8F0;
|
||||||
|
border-radius: 8px;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.testbed-app-panel #apps:hover:not(:disabled) {
|
||||||
|
border-color: #94A3B8;
|
||||||
|
background: #FFFFFF;
|
||||||
|
}
|
||||||
|
.testbed-app-panel #apps:focus {
|
||||||
|
outline: none;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-color: #0049b4;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 73, 180, 0.14);
|
||||||
|
}
|
||||||
|
.testbed-app-panel #apps:disabled {
|
||||||
|
color: #94A3B8;
|
||||||
|
background: #F8FAFC;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.testbed-app-panel .testbed-app-notice {
|
||||||
|
margin: 16px 0 0;
|
||||||
|
padding: 11px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #64748B;
|
||||||
|
background: #EFF6FF;
|
||||||
|
border: 1px solid rgba(0, 73, 180, 0.18);
|
||||||
|
border-left: 3px solid #0049b4;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.testbed-app-panel .testbed-app-notice::before {
|
||||||
|
content: "ⓘ ";
|
||||||
|
color: #0049b4;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.testbed-app-panel {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.testbed-app-panel .testbed-app-field {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.api-overview-card {
|
.api-overview-card {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
|||||||
html {
|
html {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: -moz-scrollbars-vertical;
|
overflow: -moz-scrollbars-vertical;
|
||||||
height: 100%;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@@ -12,4 +12,5 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,6 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-US">
|
<html lang="en-US">
|
||||||
<head>
|
|
||||||
<title>Swagger UI: OAuth2 Redirect</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script src="oauth2-redirect.js"></script>
|
||||||
'use strict';
|
|
||||||
function run () {
|
|
||||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
|
||||||
var sentState = oauth2.state;
|
|
||||||
var redirectUrl = oauth2.redirectUrl;
|
|
||||||
var isValid, qp, arr;
|
|
||||||
|
|
||||||
if (/code|token|error/.test(window.location.hash)) {
|
|
||||||
qp = window.location.hash.substring(1).replace('?', '&');
|
|
||||||
} else {
|
|
||||||
qp = location.search.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
arr = qp.split("&");
|
|
||||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
|
||||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
|
||||||
function (key, value) {
|
|
||||||
return key === "" ? value : decodeURIComponent(value);
|
|
||||||
}
|
|
||||||
) : {};
|
|
||||||
|
|
||||||
isValid = qp.state === sentState;
|
|
||||||
|
|
||||||
if ((
|
|
||||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
|
||||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
|
||||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
|
||||||
) && !oauth2.auth.code) {
|
|
||||||
if (!isValid) {
|
|
||||||
oauth2.errCb({
|
|
||||||
authId: oauth2.auth.name,
|
|
||||||
source: "auth",
|
|
||||||
level: "warning",
|
|
||||||
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (qp.code) {
|
|
||||||
delete oauth2.state;
|
|
||||||
oauth2.auth.code = qp.code;
|
|
||||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
|
||||||
} else {
|
|
||||||
let oauthErrorMsg;
|
|
||||||
if (qp.error) {
|
|
||||||
oauthErrorMsg = "["+qp.error+"]: " +
|
|
||||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
|
||||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
oauth2.errCb({
|
|
||||||
authId: oauth2.auth.name,
|
|
||||||
source: "auth",
|
|
||||||
level: "error",
|
|
||||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
|
||||||
}
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState !== 'loading') {
|
|
||||||
run();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
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
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
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
@@ -602,6 +602,130 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Testbed 앱 선택 패널 (DJPGPT0001)
|
||||||
|
.testbed-app-panel {
|
||||||
|
margin: $spacing-sm 0 $spacing-lg;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
background: $white;
|
||||||
|
border: 1px solid $border-gray;
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
|
||||||
|
.testbed-app-panel__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $spacing-xs $spacing-sm;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testbed-app-panel__title {
|
||||||
|
position: relative;
|
||||||
|
padding-left: $spacing-md;
|
||||||
|
font-size: $font-size-base;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
color: $text-dark;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 15px;
|
||||||
|
background: $primary-blue;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.testbed-app-panel__desc {
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $text-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testbed-app-field {
|
||||||
|
position: relative;
|
||||||
|
max-width: 380px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: $spacing-md;
|
||||||
|
top: 50%;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
margin-top: -6px;
|
||||||
|
border-right: 2px solid $text-gray;
|
||||||
|
border-bottom: 2px solid $text-gray;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#apps {
|
||||||
|
width: 100%;
|
||||||
|
height: 46px;
|
||||||
|
padding: 0 42px 0 $input-padding-x;
|
||||||
|
font-family: $font-family-primary;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $text-dark;
|
||||||
|
background: $gray-bg;
|
||||||
|
border: 1px solid $border-gray;
|
||||||
|
border-radius: $border-radius-md;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: $transition-fast;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: $text-light;
|
||||||
|
background: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
background: $white;
|
||||||
|
border-color: $primary-blue;
|
||||||
|
box-shadow: 0 0 0 3px rgba($primary-blue, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: $text-light;
|
||||||
|
background: $gray-bg;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.testbed-app-notice {
|
||||||
|
margin: $spacing-md 0 0;
|
||||||
|
padding: 11px $spacing-md;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
line-height: $line-height-normal;
|
||||||
|
color: $text-gray;
|
||||||
|
background: $light-bg;
|
||||||
|
border: 1px solid rgba($primary-blue, 0.18);
|
||||||
|
border-left: 3px solid $primary-blue;
|
||||||
|
border-radius: $border-radius-sm;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "ⓘ ";
|
||||||
|
color: $primary-blue;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-sm) {
|
||||||
|
padding: $spacing-md;
|
||||||
|
|
||||||
|
.testbed-app-field {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// API Overview Card (Flat Style)
|
// API Overview Card (Flat Style)
|
||||||
.api-overview-card {
|
.api-overview-card {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
@@ -147,6 +147,21 @@
|
|||||||
<div class="tab-content" id="testbed-tab">
|
<div class="tab-content" id="testbed-tab">
|
||||||
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/swagger-ui.css}"/>
|
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/swagger-ui.css}"/>
|
||||||
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/index.css}"/>
|
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/index.css}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/djb-swagger-testbed.css}"/>
|
||||||
|
|
||||||
|
<!-- DJPGPT0001: 앱(인증키) 선택 → 인증 정보 자동 주입 -->
|
||||||
|
<div class="testbed-app-panel" id="appsWrap">
|
||||||
|
<div class="testbed-app-panel__head">
|
||||||
|
<span class="testbed-app-panel__title">앱 선택</span>
|
||||||
|
<span class="testbed-app-panel__desc">테스트할 앱(인증키)을 선택하면 인증 정보가 자동 입력됩니다.</span>
|
||||||
|
</div>
|
||||||
|
<div class="testbed-app-field">
|
||||||
|
<select id="apps">
|
||||||
|
<option value="">앱을 선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="testbed-app-notice" id="appsNotice" style="display:none;"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="swagger-ui"></div>
|
<div id="swagger-ui"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,6 +175,8 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
<th:block layout:fragment="contentScript">
|
<th:block layout:fragment="contentScript">
|
||||||
|
<!-- DJB Swagger UI 한글화 패치 (MutationObserver DOM patch) -->
|
||||||
|
<script th:src="@{/plugins/swaggerUI/djb-swagger-i18n.js}" charset="UTF-8"></script>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
@@ -284,7 +301,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const swaggerUrl = `/api/apis/${apiId}/swagger.json`;
|
const swaggerUrl = `/djb/testbed/apis/${apiId}/swagger.json`;
|
||||||
|
|
||||||
// Load Swagger UI scripts dynamically
|
// Load Swagger UI scripts dynamically
|
||||||
const loadScript = (src) => {
|
const loadScript = (src) => {
|
||||||
@@ -305,54 +322,13 @@
|
|||||||
// Load code template script
|
// Load code template script
|
||||||
return loadScript(/*[[@{/js/code_template.js}]]*/ '');
|
return loadScript(/*[[@{/js/code_template.js}]]*/ '');
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// Initialize Swagger UI
|
// DJB 요청 스니펫(7종) / 상시 스니펫 패널 / 커스텀 응답 그리드 스크립트
|
||||||
const sampleCodeBaseUrl = /*[[@{/api/sample_code/}]]*/ '/api/sample_code/';
|
return Promise.all([
|
||||||
const generateSnippet = (parsedRequest, language) => {
|
loadScript(/*[[@{/plugins/swaggerUI/djb-swagger-snippets.js}]]*/ ''),
|
||||||
let result;
|
loadScript(/*[[@{/plugins/swaggerUI/djb-swagger-snippet-panel.js}]]*/ ''),
|
||||||
try {
|
loadScript(/*[[@{/plugins/swaggerUI/djb-swagger-response.js}]]*/ '')
|
||||||
$.ajax({
|
]);
|
||||||
url: sampleCodeBaseUrl + language,
|
}).then(() => {
|
||||||
type: 'POST',
|
|
||||||
async: false,
|
|
||||||
contentType: 'application/json',
|
|
||||||
headers: {
|
|
||||||
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ ''
|
|
||||||
},
|
|
||||||
data: JSON.stringify(parsedRequest),
|
|
||||||
success: function(data) {
|
|
||||||
result = data;
|
|
||||||
},
|
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
|
||||||
console.error('Error:', errorThrown);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SnippetGeneratorPlugin = {
|
|
||||||
fn: {
|
|
||||||
requestSnippetGenerator_csharp: (request) => {
|
|
||||||
var parsedRequest = parseSwaggerRequest(request);
|
|
||||||
return generateSnippet(parsedRequest, 'csharp');
|
|
||||||
},
|
|
||||||
requestSnippetGenerator_ecma5: (request) => {
|
|
||||||
var parsedRequest = parseSwaggerRequest(request);
|
|
||||||
return generateSnippet(parsedRequest, 'ecma5');
|
|
||||||
},
|
|
||||||
requestSnippetGenerator_java: (request) => {
|
|
||||||
var parsedRequest = parseSwaggerRequest(request);
|
|
||||||
return generateSnippet(parsedRequest, 'java');
|
|
||||||
},
|
|
||||||
requestSnippetGenerator_python: (request) => {
|
|
||||||
var parsedRequest = parseSwaggerRequest(request);
|
|
||||||
return generateSnippet(parsedRequest, 'python');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear existing Swagger UI
|
// Clear existing Swagger UI
|
||||||
document.getElementById('swagger-ui').innerHTML = '';
|
document.getElementById('swagger-ui').innerHTML = '';
|
||||||
|
|
||||||
@@ -387,48 +363,154 @@
|
|||||||
}
|
}
|
||||||
return req;
|
return req;
|
||||||
},
|
},
|
||||||
|
responseInterceptor: function(res) {
|
||||||
|
// 커스텀 응답 그리드에 렌더 (기본 응답영역은 CSS 로 숨김)
|
||||||
|
try { window.DjbSwaggerResponse.renderResponse(res); } catch (e) { console.error(e); }
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
onComplete: function() {
|
||||||
|
// 단일 API 페이지 — 상시 요청 스니펫 패널 마운트
|
||||||
|
setTimeout(function() { try { window.DjbSwaggerSnippetPanel.mount({ rootSel: '#swagger-ui' }); } catch (e) { console.error(e); } }, 50);
|
||||||
|
},
|
||||||
showMutatedRequest: false,
|
showMutatedRequest: false,
|
||||||
validatorUrl: '',
|
validatorUrl: '',
|
||||||
deepLinking: true,
|
deepLinking: true,
|
||||||
|
// 항상 1개 API 노출 → operation 을 펼친 상태로, Try it out 활성 기본
|
||||||
|
docExpansion: 'full',
|
||||||
|
defaultModelsExpandDepth: -1,
|
||||||
|
defaultModelExpandDepth: 5,
|
||||||
|
tryItOutEnabled: true,
|
||||||
|
displayRequestDuration: true,
|
||||||
presets: [
|
presets: [
|
||||||
SwaggerUIBundle.presets.apis,
|
SwaggerUIBundle.presets.apis,
|
||||||
SwaggerUIStandalonePreset.slice(1)
|
SwaggerUIStandalonePreset.slice(1)
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
SwaggerUIBundle.plugins.DownloadUrl,
|
SwaggerUIBundle.plugins.DownloadUrl,
|
||||||
SnippetGeneratorPlugin
|
window.SnippetGeneratorPlugin
|
||||||
],
|
],
|
||||||
layout: 'StandaloneLayout',
|
layout: 'StandaloneLayout',
|
||||||
requestSnippetsEnabled: true,
|
requestSnippetsEnabled: true,
|
||||||
requestSnippets: {
|
requestSnippets: {
|
||||||
generators: {
|
generators: {
|
||||||
'csharp': {
|
'curl_bash': { title: 'cURL (Bash)', syntax: 'bash' },
|
||||||
title: 'C#',
|
'curl_powershell': { title: 'cURL (PowerShell)', syntax: 'powershell' },
|
||||||
syntax: 'csharp'
|
'curl_cmd': { title: 'cURL (CMD)', syntax: 'bash' },
|
||||||
},
|
'csharp': { title: 'C#', syntax: 'csharp' },
|
||||||
'ecma5': {
|
'ecma5': { title: 'ECMA5 (JavaScript)', syntax: 'javascript' },
|
||||||
title: 'ECMA5',
|
'java': { title: 'Java', syntax: 'java' },
|
||||||
syntax: 'javascript'
|
'python': { title: 'Python', syntax: 'python' }
|
||||||
},
|
|
||||||
'java': {
|
|
||||||
title: 'Java',
|
|
||||||
syntax: 'java'
|
|
||||||
},
|
|
||||||
'python': {
|
|
||||||
title: 'Python',
|
|
||||||
syntax: 'python'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
defaultExpanded: true,
|
defaultExpanded: true,
|
||||||
languages: null
|
languages: null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.ui = ui;
|
window.ui = ui;
|
||||||
|
// Execute 클릭 캡처(응답 그리드 대상/타이머) 등록 — #swagger-ui 는 재초기화돼도 유지됨
|
||||||
|
window.DjbSwaggerResponse.attach('#swagger-ui');
|
||||||
|
// 앱이 이미 선택돼 있으면 스펙 로드 후 재주입
|
||||||
|
if (appsSelect && appsSelect.value) {
|
||||||
|
setTimeout(function () { onAppSelected(appsSelect.value); }, 300);
|
||||||
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Failed to load Swagger UI:', error);
|
console.error('Failed to load Swagger UI:', error);
|
||||||
document.getElementById('swagger-ui').innerHTML = '<p>Swagger UI를 불러오는 중 오류가 발생했습니다.</p>';
|
document.getElementById('swagger-ui').innerHTML = '<p>Swagger UI를 불러오는 중 오류가 발생했습니다.</p>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== DJPGPT0001: 앱 인증 정보 자동 주입 =====
|
||||||
|
const DEFAULT_TOKEN_API_ID = 'default-token-api-spec';
|
||||||
|
const appsSelect = document.getElementById('apps');
|
||||||
|
|
||||||
|
function eligibilityMessage(reason) {
|
||||||
|
switch (reason) {
|
||||||
|
case 'ANONYMOUS': return '로그인 후 본인 앱의 인증 정보를 사용할 수 있습니다.';
|
||||||
|
case 'INDIVIDUAL_USER': return '개인사용자는 앱 인증 정보를 사용할 수 없습니다. 법인 계정으로 이용해 주세요.';
|
||||||
|
case 'NO_CREDENTIAL': return '사용 가능한 앱(인증키)이 없습니다. 인증 키 관리에서 앱을 생성해 주세요.';
|
||||||
|
default: return '앱 인증 정보를 사용할 수 없습니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTestbedContext() {
|
||||||
|
fetch(/*[[@{/djb/testbed/auth/context}]]*/ '/djb/testbed/auth/context')
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (ctx) {
|
||||||
|
window.__djbGateway = ctx.gateway || {};
|
||||||
|
if (!appsSelect) return;
|
||||||
|
const notice = document.getElementById('appsNotice');
|
||||||
|
if (!ctx.eligible) {
|
||||||
|
appsSelect.disabled = true;
|
||||||
|
if (notice) { notice.style.display = 'block'; notice.textContent = eligibilityMessage(ctx.reason); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appsSelect.disabled = false;
|
||||||
|
if (notice) notice.style.display = 'none';
|
||||||
|
(ctx.credentials || []).forEach(function (c) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = c.clientId;
|
||||||
|
opt.textContent = c.clientName ? (c.clientName + ' (' + c.clientId + ')') : c.clientId;
|
||||||
|
appsSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function (e) { console.error('테스트베드 컨텍스트 로드 실패', e); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchOAuthToken(clientId, clientSecret, gw) {
|
||||||
|
const body = 'grant_type=client_credentials'
|
||||||
|
+ '&client_id=' + encodeURIComponent(clientId)
|
||||||
|
+ '&client_secret=' + encodeURIComponent(clientSecret)
|
||||||
|
+ '&scope=api';
|
||||||
|
return fetch(window.location.origin + (/*[[@{/api/call-api}]]*/ '/api/call-api'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'original-url': gw.tokenUrl,
|
||||||
|
'X-XSRF-TOKEN': /*[[${_csrf.token}]]*/ ''
|
||||||
|
},
|
||||||
|
body: body
|
||||||
|
}).then(function (r) { return r.ok ? r.json() : null; })
|
||||||
|
.then(function (data) { return data ? data.access_token : null; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function authorizeApp(secret) {
|
||||||
|
const gw = window.__djbGateway || {};
|
||||||
|
if (!window.ui) return;
|
||||||
|
if (secret.authType === 'OAUTH') {
|
||||||
|
fetchOAuthToken(secret.clientId, secret.clientSecret, gw).then(function (token) {
|
||||||
|
if (!token) return;
|
||||||
|
window.ui.authActions.authorize({
|
||||||
|
djbOAuth: {
|
||||||
|
name: 'djbOAuth',
|
||||||
|
schema: { type: 'apiKey', in: 'header', name: gw.oauthTokenHeader },
|
||||||
|
value: 'Bearer ' + token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (secret.authType === 'API_KEY') {
|
||||||
|
window.ui.authActions.authorize({
|
||||||
|
djbApiKey: {
|
||||||
|
name: 'djbApiKey',
|
||||||
|
schema: { type: 'apiKey', in: 'header', name: gw.apiKeyHeader },
|
||||||
|
value: secret.clientSecret
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAppSelected(clientId) {
|
||||||
|
if (!clientId) return;
|
||||||
|
if (!currentApiId || currentApiId === 'default' || currentApiId === DEFAULT_TOKEN_API_ID) return;
|
||||||
|
fetch((/*[[@{/djb/testbed/auth/credentials/}]]*/ '/djb/testbed/auth/credentials/')
|
||||||
|
+ encodeURIComponent(clientId) + '/secret?apiId=' + encodeURIComponent(currentApiId))
|
||||||
|
.then(function (r) { return r.ok ? r.json() : null; })
|
||||||
|
.then(function (secret) { if (secret) authorizeApp(secret); })
|
||||||
|
.catch(function (e) { console.error('앱 인증정보 주입 실패', e); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appsSelect) {
|
||||||
|
appsSelect.addEventListener('change', function () { onAppSelected(this.value); });
|
||||||
|
}
|
||||||
|
loadTestbedContext();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<section layout:fragment="contentFragment" class="content">
|
<section layout:fragment="contentFragment" class="content">
|
||||||
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/swagger-ui.css}"/>
|
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/swagger-ui.css}"/>
|
||||||
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/index.css}"/>
|
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/index.css}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" th:href="@{/plugins/swaggerUI/djb-swagger-testbed.css}"/>
|
||||||
|
|
||||||
<div class="content_wrap">
|
<div class="content_wrap">
|
||||||
<div class="sub_title3">
|
<div class="sub_title3">
|
||||||
@@ -40,54 +41,14 @@
|
|||||||
|
|
||||||
<th:block layout:fragment="contentScript">
|
<th:block layout:fragment="contentScript">
|
||||||
<script th:src="@{/js/code_template.js}"></script>
|
<script th:src="@{/js/code_template.js}"></script>
|
||||||
|
<!-- DJB Swagger UI 한글화 패치 (MutationObserver DOM patch) -->
|
||||||
|
<script th:src="@{/plugins/swaggerUI/djb-swagger-i18n.js}" charset="UTF-8"></script>
|
||||||
|
<!-- DJB 요청 스니펫(클라이언트 7종) / 상시 스니펫 패널 / 커스텀 응답 그리드 -->
|
||||||
|
<script th:src="@{/plugins/swaggerUI/djb-swagger-snippets.js}" charset="UTF-8"></script>
|
||||||
|
<script th:src="@{/plugins/swaggerUI/djb-swagger-snippet-panel.js}" charset="UTF-8"></script>
|
||||||
|
<script th:src="@{/plugins/swaggerUI/djb-swagger-response.js}" charset="UTF-8"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const generateSnippet = (parsedRequest, language) => {
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
$.ajax({
|
|
||||||
url: '[[@{/api/sample_code/}]]' + language,
|
|
||||||
type: 'POST',
|
|
||||||
async: false,
|
|
||||||
contentType: 'application/json',
|
|
||||||
headers: {
|
|
||||||
'X-XSRF-TOKEN': '[[${_csrf.token}]]'
|
|
||||||
},
|
|
||||||
data: JSON.stringify(parsedRequest),
|
|
||||||
success: function(data) {
|
|
||||||
result = data;
|
|
||||||
},
|
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
|
||||||
console.error('Error:', errorThrown);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SnippetGeneratorPlugin = {
|
|
||||||
fn: {
|
|
||||||
requestSnippetGenerator_csharp: (request) => {
|
|
||||||
var parsedRequest = parseSwaggerRequest(request);
|
|
||||||
return generateSnippet(parsedRequest, 'csharp');
|
|
||||||
},
|
|
||||||
requestSnippetGenerator_ecma5: (request) => {
|
|
||||||
var parsedRequest = parseSwaggerRequest(request);
|
|
||||||
return generateSnippet(parsedRequest, 'ecma5');
|
|
||||||
},
|
|
||||||
requestSnippetGenerator_java: (request) => {
|
|
||||||
var parsedRequest = parseSwaggerRequest(request);
|
|
||||||
return generateSnippet(parsedRequest, 'java');
|
|
||||||
},
|
|
||||||
requestSnippetGenerator_python: (request) => {
|
|
||||||
var parsedRequest = parseSwaggerRequest(request);
|
|
||||||
return generateSnippet(parsedRequest, 'python');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
let currentApiId = '[[${id}]]';
|
let currentApiId = '[[${id}]]';
|
||||||
|
|
||||||
@@ -131,43 +92,51 @@
|
|||||||
}
|
}
|
||||||
return req;
|
return req;
|
||||||
},
|
},
|
||||||
|
responseInterceptor: function(res) {
|
||||||
|
// 커스텀 응답 그리드에 렌더 (기본 응답영역은 CSS 로 숨김)
|
||||||
|
try { window.DjbSwaggerResponse.renderResponse(res); } catch (e) { console.error(e); }
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
onComplete: function() {
|
||||||
|
// 단일 API 페이지 — 상시 요청 스니펫 패널 마운트
|
||||||
|
setTimeout(function() { try { window.DjbSwaggerSnippetPanel.mount({ rootSel: '#swagger-ui' }); } catch (e) { console.error(e); } }, 50);
|
||||||
|
},
|
||||||
showMutatedRequest: false,
|
showMutatedRequest: false,
|
||||||
validatorUrl: '',
|
validatorUrl: '',
|
||||||
deepLinking: true,
|
deepLinking: true,
|
||||||
|
// 항상 1개 API 노출 → operation 을 펼친 상태로, Try it out 활성 기본
|
||||||
|
docExpansion: 'full',
|
||||||
|
defaultModelsExpandDepth: -1,
|
||||||
|
defaultModelExpandDepth: 5,
|
||||||
|
tryItOutEnabled: true,
|
||||||
|
displayRequestDuration: true,
|
||||||
presets: [
|
presets: [
|
||||||
SwaggerUIBundle.presets.apis,
|
SwaggerUIBundle.presets.apis,
|
||||||
SwaggerUIStandalonePreset.slice(1)
|
SwaggerUIStandalonePreset.slice(1)
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
SwaggerUIBundle.plugins.DownloadUrl,
|
SwaggerUIBundle.plugins.DownloadUrl,
|
||||||
SnippetGeneratorPlugin
|
window.SnippetGeneratorPlugin
|
||||||
],
|
],
|
||||||
layout: 'StandaloneLayout',
|
layout: 'StandaloneLayout',
|
||||||
requestSnippetsEnabled: true,
|
requestSnippetsEnabled: true,
|
||||||
requestSnippets: {
|
requestSnippets: {
|
||||||
generators: {
|
generators: {
|
||||||
'csharp': {
|
'curl_bash': { title: 'cURL (Bash)', syntax: 'bash' },
|
||||||
title: 'C#',
|
'curl_powershell': { title: 'cURL (PowerShell)', syntax: 'powershell' },
|
||||||
syntax: 'csharp'
|
'curl_cmd': { title: 'cURL (CMD)', syntax: 'bash' },
|
||||||
},
|
'csharp': { title: 'C#', syntax: 'csharp' },
|
||||||
'ecma5': {
|
'ecma5': { title: 'ECMA5 (JavaScript)', syntax: 'javascript' },
|
||||||
title: 'ECMA5',
|
'java': { title: 'Java', syntax: 'java' },
|
||||||
syntax: 'javascript'
|
'python': { title: 'Python', syntax: 'python' }
|
||||||
},
|
|
||||||
'java': {
|
|
||||||
title: 'Java',
|
|
||||||
syntax: 'java'
|
|
||||||
},
|
|
||||||
'python': {
|
|
||||||
title: 'Python',
|
|
||||||
syntax: 'python'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
defaultExpanded: true,
|
defaultExpanded: true,
|
||||||
languages: null
|
languages: null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.ui = ui;
|
window.ui = ui;
|
||||||
|
// Execute 클릭 캡처(응답 그리드 대상/타이머) 등록 — #swagger-ui 는 재초기화돼도 유지됨
|
||||||
|
window.DjbSwaggerResponse.attach('#swagger-ui');
|
||||||
}
|
}
|
||||||
const apiList = document.getElementById('apiList');
|
const apiList = document.getElementById('apiList');
|
||||||
createCustomSelect(apiList, {
|
createCustomSelect(apiList, {
|
||||||
|
|||||||
Reference in New Issue
Block a user