스크립트 추가

This commit is contained in:
현성필
2023-12-27 21:09:01 +09:00
parent 8eae737de0
commit 5c938336b3
7 changed files with 396 additions and 77 deletions
@@ -22,6 +22,10 @@ public class ApiRequestDTO {
private String requestBody;
private String preRequestScript;
private String postRequestScript;
public String getId() {
return id;
}
@@ -93,4 +97,20 @@ public class ApiRequestDTO {
public void setRequestBody(String requestBody) {
this.requestBody = requestBody;
}
public String getPreRequestScript() {
return preRequestScript;
}
public void setPreRequestScript(String preRequestScript) {
this.preRequestScript = preRequestScript;
}
public String getPostRequestScript() {
return postRequestScript;
}
public void setPostRequestScript(String postRequestScript) {
this.postRequestScript = postRequestScript;
}
}
@@ -40,6 +40,13 @@ public class ApiRequestInfo {
@Lob
private String requestBody;
@Lob
private String preRequestScript;
@Lob
private String postRequestScript;
public String getName() {
return name;
}
@@ -120,6 +127,22 @@ public class ApiRequestInfo {
this.requestBody = requestBody;
}
public String getPreRequestScript() {
return preRequestScript;
}
public void setPreRequestScript(String preRequestScript) {
this.preRequestScript = preRequestScript;
}
public String getPostRequestScript() {
return postRequestScript;
}
public void setPostRequestScript(String postRequestScript) {
this.postRequestScript = postRequestScript;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -130,7 +153,7 @@ public class ApiRequestInfo {
@Override
public int hashCode() {
return Objects.hash(id, server, owner, method, path, headers, queryParams, requestBody);
return Objects.hash(id, server, owner, method, path, headers, queryParams, requestBody, preRequestScript, postRequestScript);
}
}
@@ -70,6 +70,9 @@ public class ApiRequestMgmtService {
existing.setVariables(api.getVariables());
existing.setServer(api.getServer());
existing.setRequestBody(api.getRequestBody());
existing.setPreRequestScript(api.getPreRequestScript());
existing.setPostRequestScript(api.getPostRequestScript());
apiRequestRepository.save(existing);
api = existing;
}
@@ -37,7 +37,7 @@ public class NettyApiClient {
ServerRepository serverRepository;
public CompletableFuture<ApiResponse> handleRequest(ApiRequestDTO originalRequest) throws InterruptedException, InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchMethodException {
ApiRequestDTO apiRequest = replacePlaceholdersWithVariables(originalRequest);
ApiRequestDTO apiRequest = originalRequest;
Server server = serverRepository.findById(apiRequest.getServer()).orElseThrow(() -> new ServerNotFoundException("Server not found"));
String host = server.getHostname();
int port = server.getPort();
@@ -108,37 +108,4 @@ public class NettyApiClient {
return responseFuture;
}
private ApiRequestDTO replacePlaceholdersWithVariables(ApiRequestDTO original) throws InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchMethodException {
ApiRequestDTO request = (ApiRequestDTO) BeanUtils.cloneBean(original);
if (request.getVariables() != null) {
for (ApiRequestKeyValueDTO variable : request.getVariables()) {
if (variable.isEnabled()) {
String placeholder = "{{" + variable.getKey() + "}}";
// Replace in path
request.setPath(request.getPath().replace(placeholder, variable.getValue()));
// Replace in requestBody
if (request.getRequestBody() != null) {
request.setRequestBody(request.getRequestBody().replace(placeholder, variable.getValue()));
}
// Replace in headers
if (request.getHeaders() != null) {
for (ApiRequestKeyValueDTO header : request.getHeaders()) {
header.setKey(header.getKey().replace(placeholder, variable.getValue()));
header.setValue(header.getValue().replace(placeholder, variable.getValue()));
}
}
// Replace in queryParams
if (request.getQueryParams() != null) {
for (ApiRequestKeyValueDTO queryParam : request.getQueryParams()) {
queryParam.setKey(queryParam.getKey().replace(placeholder, variable.getValue()));
queryParam.setValue(queryParam.getValue().replace(placeholder, variable.getValue()));
}
}
}
}
}
return request;
}
}
+225 -42
View File
@@ -14,7 +14,11 @@ class APITester {
this.collections = []; //API 컬렉션
this.requestEditors = []; //API request body editor
this.responseEditors = []; //API response body editor
this.preRequestEditors = [];
this.postRequestEditors = [];
window.globals = {};
this.globalVariables = window.globals;
this.variables = {};
}
renderServers(selectedServer) {
@@ -36,7 +40,7 @@ class APITester {
apiRequestTemplate(apiRequest, index) {
return `
<div class="tab-pane fade mt-1 api_request" id="api_request_${apiRequest.id}" data-index="${index}">
<div class="tab-pane fade mt-1 api_request" id="api_request_${apiRequest.id}">
<div><span class="collection_name">${apiRequest.collectionName}</span> &gt; <span class="api_name">${apiRequest.name}</span></div>
<div class="d-flex align-items-center api_request_info">
<div class="form-floating col-md-1 me-1">
@@ -60,13 +64,12 @@ class APITester {
<div class="form-floating col-md-2 me-1">
<div class="form-floating col-md">
<input type="text" name="basePath" class="form-control" value="${this.basePath(apiRequest.server)}" readonly>
<label class="must">base path</label>
<label class="must">기본 경로</label>
</div>
</div>
<div class="input-group col-md me-1">
<div class="form-floating col-md">
<input type="text" name="path" class="form-control path" value="${apiRequest.path}" placeholder="호출할 경로 (/부터 입력)"
required>
<input type="text" name="path" class="form-control path" value="${apiRequest.path}" placeholder="호출할 경로 (/부터 입력)">
<label class="must">Path</label>
</div>
<button class="btn btn-primary send">Send</button>
@@ -98,6 +101,19 @@ class APITester {
aria-selected="false">Variables
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link btn_prerequest_tab" data-bs-toggle="tab"
type="button" role="tab" aria-controls="preScrit" data-bs-target="#preRequestTab_${apiRequest.id}"
aria-selected="false">Pre-Request
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link btn_postrequest_tab" data-bs-toggle="tab"
type="button" role="tab" aria-controls="postScript" data-bs-target="#postRequestTab_${apiRequest.id}"
aria-selected="false">Post-Request
</button>
</li>
</ul>
<div class="tab-content border-bottom" style="min-height: 300px;">
<div class="tab-pane fade show request_param_tab" role="tabpanel" id="requestParamTab_${apiRequest.id}"
@@ -164,6 +180,14 @@ class APITester {
</select>
<div class="request_body mt-1" style="width:100%;height:250px;border:1px solid grey;"></div>
</div>
<div class="tab-pane fade pre_script_tab" role="tabpanel" id="preRequestTab_${apiRequest.id}"
aria-labelledby="preScrit-tab">
<div class="pre-request mt-1" style="width:100%;height:250px;border:1px solid grey;"></div>
</div>
<div class="tab-pane fade post_script_tab" role="tabpanel" id="postRequestTab_${apiRequest.id}"
aria-labelledby="postScrit-tab">
<div class="post-request mt-1" style="width:100%;height:250px;border:1px solid grey;"></div>
</div>
</div>
<div class="mt-1">
<span>Response</span>
@@ -467,8 +491,7 @@ class APITester {
//console.log(fieldName);
const newValue = this.getFieldValue(element);
//console.log(newValue);
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
let model = this.apiRequests[tabIndex];
if (fieldName !== '') {
@@ -479,7 +502,7 @@ class APITester {
}
handleUpdateServer(event) {
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
let model = this.apiRequests[tabIndex];
let target = $('#api_request_' + model.id);
//console.log('model.server: ' + model.server);
@@ -505,7 +528,7 @@ class APITester {
for (let idx = 0; idx < this.apiRequests.length; idx++) {
let li = `<li class="nav-item" role="presentation">
<button class="nav-link" id="api_request_tab_${this.apiRequests[idx].id}" data-index="${idx}" data-bs-toggle="tab" type="button" role="tab" aria-controls="api_request_${this.apiRequests[idx].id}" data-bs-target="#api_request_${this.apiRequests[idx].id}" aria-selected="false">${this.apiRequests[idx].name}
<button class="nav-link" id="api_request_tab_${this.apiRequests[idx].id}" data-bs-toggle="tab" type="button" role="tab" aria-controls="api_request_${this.apiRequests[idx].id}" data-bs-target="#api_request_${this.apiRequests[idx].id}" aria-selected="false">${this.apiRequests[idx].name}
<span class="close_tab"><i class="fa fa-light fa-close"></i></span>
</button>
</li>`;
@@ -524,7 +547,7 @@ class APITester {
this.openTab(model.id);
let target = $('#api_request_' + model.id);
require(['vs/editor/editor.main'], function () {
require(['vs/editor/editor.main'], () => {
let requestBodyEditor = monaco.editor.create(target.find(".request_body")[0], {
value: model.requestBody,
language: 'json',
@@ -532,15 +555,15 @@ class APITester {
automaticLayout: true
});
me.requestEditors.push(requestBodyEditor);
target.find(".request_body")[0].addEventListener('shown.bs.tab', function () {
target.find(".request_body")[0].addEventListener('shown.bs.tab', () => {
me.requestEditors[index].layout();
});
me.requestEditors[index].onDidChangeModelContent(function (e) {
me.requestEditors[index].onDidChangeModelContent( (e) => {
model.requestBody = requestBodyEditor.getValue();
});
target.find('.request_body_type').on('change', function () {
target.find('.request_body_type').on('change', () => {
let model = requestBodyEditor.getModel();
monaco.editor.setModelLanguage(model, $(this).val());
});
@@ -552,6 +575,40 @@ class APITester {
automaticLayout: true
});
me.responseEditors.push(responseBodyEditor);
let preRequestEditor = monaco.editor.create(target.find(".pre-request")[0], {
value: model.preRequestScript,
language: 'javascript',
theme: 'vs-light',
automaticLayout: true
});
me.preRequestEditors.push(preRequestEditor);
target.find(".pre-request")[0].addEventListener('shown.bs.tab', () => {
me.preRequestEditors[index].layout();
});
me.preRequestEditors[index].onDidChangeModelContent( (e) => {
model.preRequestScript = preRequestEditor.getValue();
});
let postRequestEditor = monaco.editor.create(target.find(".post-request")[0], {
value: model.postRequestScript,
language: 'javascript',
theme: 'vs-light',
automaticLayout: true
});
me.postRequestEditors.push(postRequestEditor);
target.find(".post-request")[0].addEventListener('shown.bs.tab', () => {
me.postRequestEditors[index].layout();
});
me.postRequestEditors[index].onDidChangeModelContent( (e) => {
model.postRequestScript = postRequestEditor.getValue();
});
});
}
@@ -621,7 +678,7 @@ class APITester {
}
addHeader(event) {
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
let model = this.apiRequests[tabIndex];
model.headers.push({
enabled: true,
@@ -632,7 +689,7 @@ class APITester {
}
removeHeader(event) {
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
const index = $(event.currentTarget).closest('tr').data('index');
let model = this.apiRequests[tabIndex];
model.headers.splice(index, 1);
@@ -649,7 +706,7 @@ class APITester {
addQueryParam(event) {
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
//console.log('addQueryParam');
let model = this.apiRequests[tabIndex];
model.queryParams.push({
@@ -663,7 +720,7 @@ class APITester {
removeQueryParam(event) {
//console.log('removeQueryParam');
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
const index = $(event.currentTarget).closest('tr').data('index');
let model = this.apiRequests[tabIndex];
model.queryParams.splice(index, 1);
@@ -680,7 +737,7 @@ class APITester {
addVariable(event) {
//console.log('addVariable');
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
let model = this.apiRequests[tabIndex];
model.variables.push({
enabled: true,
@@ -692,7 +749,7 @@ class APITester {
removeVariable(event) {
//console.log('removeVariable');
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
const index = $(event.currentTarget).closest('tr').data('index');
let model = this.apiRequests[tabIndex];
model.variables.splice(index, 1);
@@ -707,7 +764,7 @@ class APITester {
target.find('.variables').append(this.renderVariables(model.variables));
}
parseParam(event) {
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
let model = this.apiRequests[tabIndex];
model.path = $(event.currentTarget).val();
//console.log(model.path);
@@ -729,7 +786,7 @@ class APITester {
}
}
buildPath(event) {
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
let model = this.apiRequests[tabIndex];
let queryString = model.path.split('?')[0];
if (model.queryParams.length > 0) {
@@ -757,41 +814,65 @@ class APITester {
}
sendApiRequest(event) {
const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
const tabIndex = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
console.log(tabIndex);
const me = this;
let model = this.apiRequests[tabIndex];
if (!model.server){
$('.toast-body').text('서버를 등록해주세요.');
$('.toast').toast('show');
return;
}
let target = $('#api_request_' + model.id);
target.find('.response_headers').empty();
this.responseEditors[tabIndex].setValue('');
let startTime = new Date().getTime();
if (model.path !== '') {
this.showLoadingOverlay();
this.showLoadingOverlay();
model.responseModel = {};
let processedRequest = this.executePreRequestScript(model, tabIndex,() => {
processedRequest = this.replacePlaceholdersWithVariables(processedRequest, tabIndex);
this.currentAjaxRequest = $.ajax({
url: '/mgmt/api/test.do',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(model),
success: function (data) {
data: JSON.stringify(processedRequest),
success: (data) => {
model.responseModel.status = data.status;
model.responseModel.body = data.body;
model.responseModel.headers = data.headers;
model.responseModel.size = data.size;
let response = {};
response.body = data.body;
response.status = data.status;
response.headers = {};
response.size = data.size;
_.forEach(data.headers, (header) => {
response.headers[header.key] = header.value;
});
this.executePostRequestScript(model.postRequestScript, tabIndex, response, ()=> {
let endTime = new Date().getTime();
model.responseModel.time = endTime - startTime;
me.renderResponse(tabIndex);
me.hideLoadingOverlay();
});
},
error: function (response, textStatus, errorThrown) {
error: (response, textStatus, errorThrown) => {
me.hideLoadingOverlay();
$('.toast-body').text(response.responseJSON.error);
$('.toast').toast('show');
},
complete: function () {
let endTime = new Date().getTime();
model.responseModel.time = endTime - startTime;
me.renderResponse(tabIndex);
me.hideLoadingOverlay();
}
});
}
});
}
@@ -917,7 +998,7 @@ class APITester {
saveAPIRequest(event) {
const me = this;
const index = $(event.currentTarget).data('index');
const index = $('#apiRequestTabContent').children().index($(event.currentTarget).closest('.api_request'));
let model = this.apiRequests[index];
me.showLoadingOverlay();
@@ -946,6 +1027,11 @@ class APITester {
addAPIRequest(event) {
const collectionId = $('#modal_collection').val();
let newApiName = $('#new_api_name').val();
if (newApiName === '') {
newApiName = '새로운 API';
}
const collection = this.collections.filter(function (collection) {
return collection.id === collectionId;
})[0];
@@ -954,12 +1040,14 @@ class APITester {
id: '',
server: null,
method: 'GET',
name: 'New Request',
name: newApiName,
path: '',
headers: [],
queryParams: [],
variables: [],
requestBody: '',
preRequestScript :'',
postRequestScript :'',
collectionId: collectionId,
responseModel: {
status: 0,
@@ -991,7 +1079,7 @@ class APITester {
me.renderTabHeader();
me.renderTabContent(me.currentIndex);
$('#new_api_name').val('');
me.hideLoadingOverlay();
$('#new_api_request_modal').modal('hide');
}
@@ -1078,28 +1166,123 @@ class APITester {
me.hideLoadingOverlay();
me.loadCollections();
$('#confirm_delete_api_modal').modal('hide');
// $('#confirm_delete_api_modal').find('.confirm_delete_api').removeAttr('data-id');
// $('#confirm_delete_api_modal').find('.confirm_delete_api').removeAttr('data-collection');
}
});
}
closeTabWithApiIndex(index) {
console.log(index);
if (index !== undefined){
//console.log('closeTabWithApiIndex: ' + index);
const model = this.apiRequests[index];
console.log(model);
this.apiRequests.splice(index, 1);
let apiRequestTabTitle = $(this.apiTabTitleTarget);
apiRequestTabTitle.empty();
this.renderTabHeader();
$(`#api_request_${model.id}`).remove();
$(`#apiRequestTabTitle li:eq(${this.apiRequests.length - 1}) button`).tab('show');
// $(`#apiRequestTabTitle li:eq(${this.apiRequests.length - 1}) button`).tab('show');
$(`#apiRequestTabTitle li:eq(0) button`).tab('show');
}
}
closeTab(event) {
let index = $(event.currentTarget).closest('button').data('index');
//console.log('closeTab: ' + index);
const index = $('#apiRequestTabTitle').children().index($(event.currentTarget).closest('.nav-item'));
this.closeTabWithApiIndex(index);
}
replacePlaceholdersWithVariables(original, index) {
let request = JSON.parse(JSON.stringify(original));
let mergedVariables = {};
_.forEach(this.globalVariables, (value, key) => {
mergedVariables[key] = value;
});
if (this.variables[index]) {
_.forEach(this.variables[index], (value, key) => {
mergedVariables[key] = value;
});
}
_.forEach(mergedVariables, (value, key) => {
const placeholder = `{{${key}}}`;
// Replace in path
request.path = request.path.replace(new RegExp(placeholder, 'g'), value);
// Replace in requestBody
if (request.requestBody) {
request.requestBody = request.requestBody.replace(new RegExp(placeholder, 'g'), value);
}
// Replace in headers
if (request.headers) {
request.headers = request.headers.map(header => {
return {
...header,
key: header.key.replace(new RegExp(placeholder, 'g'), value),
value: header.value.replace(new RegExp(placeholder, 'g'), value)
};
});
}
// Replace in queryParams
if (request.queryParams) {
request.queryParams = request.queryParams.map(queryParam => {
return {
...queryParam,
key: queryParam.key.replace(new RegExp(placeholder, 'g'), value),
value: queryParam.value.replace(new RegExp(placeholder, 'g'), value)
};
});
}
});
return request;
}
executePreRequestScript(model, index, callback) {
let script = model.preRequestScript;
let resultFrame = document.getElementById('preRequest');
let {headers, method, path, queryParams, requestBody} = model;
window.request = {headers, method, path, queryParams, requestBody};
this.variables[index] = {};
model.variables.forEach((item) =>{
if (item.enabled) {
this.variables[index][item.key] = item.value;
}
});
window.preRequestComplete = function(updatedModel) {
callback(updatedModel);
};
window.temp_variable = this.variables[index];
resultFrame.srcdoc = `<script>
window.globals = window.parent.globals;
window.variables = window.parent.temp_variable;
window.request = window.parent.request;
${script}
window.parent.preRequestComplete(window.request);
</script>`;
return model;
}
executePostRequestScript(script, index, response, callback) {
let resultFrame = document.getElementById('postRequest');
window.response = response;
window.parent.temp_variable = this.variables[index];
window.postRequestComplete = function(updatedModel) {
callback(updatedModel);
};
window.temp_variable = this.variables[index];
resultFrame.srcdoc = `<script>
window.globals = window.parent.globals;
window.variables = window.parent.temp_variable;
window.response = window.parent.response;
${script}
window.parent.postRequestComplete(window.request);
</script>`;
}
}
+116
View File
@@ -0,0 +1,116 @@
class LayoutUtil {
static flattenObject(obj, prefix = '') {
return _.reduce(obj, (acc, value, key) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (_.isObject(value) && !_.isArray(value)) {
_.assign(acc, this.flattenObject(value, fullKey));
} else if (_.isArray(value)) {
// Process each item in the array
value.forEach((item, index) => {
// If the item is an object, recurse, otherwise assign directly
if (_.isObject(item)) {
_.assign(acc, this.flattenObject(item, `${fullKey}[${index}]`));
} else {
acc[`${fullKey}[${index}]`] = item;
}
});
} else {
acc[fullKey] = value;
}
return acc;
}, {});
}
static createGroup(groupModel) {
let groupHtml = `<div><h6>${groupModel.groupTitle}</h6>`;
groupModel.fields.forEach(item => {
groupHtml += this.createFormElement(item);
});
groupHtml += '</div>';
return groupHtml;
}
static createTextInput(elementModel) {
return `
<div class="form-floating mb-2">
<input type="text" name="${elementModel.path}" class="form-control mb-2" id="${elementModel.path}" placeholder="${elementModel.placeholder}">
<label for="${elementModel.path}" class="form-label">${elementModel.label}</label>
</div>
`;
}
static createBooleanInput(elementModel) {
return `
<div class="form-floating mb-2">
<div class="form-check form-check-inline">
<input type="checkbox" name="${elementModel.path}" class="form-check-input mb-2" id="${elementModel.path}" placeholder="${elementModel.placeholder}">
<label class="form-check-label" for="${elementModel.path}" class="form-label">${elementModel.label}</label>
</div>
</div>`;
}
static createSelectInput(elementModel) {
const optionsHtml = elementModel.options.map(optionValue => `<option value="${optionValue}">${optionValue}</option>`).join('');
return `
<div class="form-floating mb-2">
<select class="form-select mb-2" id="${elementModel.path}" name="${elementModel.path}">
${optionsHtml}
</select>
<label for="${elementModel.path}" class="form-label">${elementModel.label}</label>
</div>
`;
}
static createTextAreaInput(elementModel) {
return `
<div class="form-floating mb-2">
<textarea class="form-control mb-2" name="${elementModel.path}" id="${elementModel.path}" placeholder="${elementModel.placeholder}"></textarea>
<label for="${elementModel.path}" class="form-label">${elementModel.label}</label>
</div>`;
}
static createFormElement(elementModel) {
switch (elementModel.type) {
case "boolean":
return this.createBooleanInput(elementModel);
case "text":
return this.createTextInput(elementModel);
case "select":
return this.createSelectInput(elementModel);
case "textarea":
return this.createTextAreaInput(elementModel);
default:
console.error("Unsupported element type: " + elementModel.type);
return '';
}
}
static createForm(model) {
let formHtml = '';
model.forEach(group => {
formHtml += this.createGroup(group);
});
return formHtml;
}
static getFieldValue(element) {
if ($(element).is('select') || $(element).is('input[type="text"]') || $(element).is('textarea')) {
return $(element).val();
}
if ($(element).is('input[type="checkbox"]')) {
return $(element).is(':checked');
}
}
static echartReplacer(key, value) {
if (key === 'echart' || key === 'series') {
return undefined;
}
return value;
}
}
@@ -48,6 +48,10 @@
</div>
<div class="modal-body">
<select class="form-select" id="modal_collection"></select>
<div class="input-group mt-2">
<span class="input-group-text">API 이름</span>
<input type="text" class="form-control" id="new_api_name">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
@@ -61,11 +65,14 @@
<li th:each="server : ${servers}" th:data-id="${server.id}" th:data-name="${server.name}" th:data-path="${server.basePath}"></li>
</ul>
</div>
<iframe id="preRequest" class="preRequest"></iframe>
<iframe id="postRequest" class="postRequest"></iframe>
</section>
</body>
<th:block layout:fragment="contentScript">
<script th:src="@{/js/APITester.js}"></script>
<script th:src="@{/js/LayoutUtil.js}"></script>
<script>
$(document).ajaxError(function(event, jqxhr, settings, exception) {
if (jqxhr.status === 401) {