diff --git a/src/main/resources/static/js/APITester.js b/src/main/resources/static/js/APITester.js
new file mode 100644
index 0000000..28bfc8b
--- /dev/null
+++ b/src/main/resources/static/js/APITester.js
@@ -0,0 +1,1080 @@
+require.config({paths: {'vs': '/plugins/vs'}});
+
+
+class APITester {
+
+
+ constructor() {
+ this.servers = [];
+ this.apiTabTitleTarget = '#apiRequestTabTitle';
+ this.apiTabContentTarget = '#apiRequestTabContent';
+ this.collectionTarget = '#side_menubar';
+ this.currentIndex = 0;
+ this.apiRequests = []; //탭 열린 API
+ this.collections = []; //API 컬렉션
+ this.requestEditors = []; //API request body editor
+ this.responseEditors = []; //API response body editor
+
+ }
+
+ renderServers(selectedServer) {
+ const optionsHtml = this.servers.map(server => `
+
+ `).join('');
+
+ return ``;
+ }
+
+ basePath(serverId) {
+ if (serverId === undefined || serverId === null) {
+ return this.servers[0].basePath;
+ }
+ return this.servers.filter(server => server.id == serverId)[0].basePath; // == : 타입 비교 없이 값만 비교
+ }
+
+ apiRequestTemplate(apiRequest, index) {
+ return `
+
+
${apiRequest.collectionName} > ${apiRequest.name}
+
+
+
+
+
+
+ ${this.renderServers(apiRequest.server)}
+
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+ | # |
+ 이름 |
+ 값 |
+
+
+ |
+
+
+
+ ${this.renderParams(apiRequest.queryParams)}
+
+
+
+
+
+
+
+
+ | # |
+ 키 |
+ 값 |
+
+
+ |
+
+
+
+ ${this.renderVariables(apiRequest.variables)}
+
+
+
+
+
+
+
+
+
+ Response
+
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+ `;
+ }
+
+ queryParamTemplate(queryParam, index) {
+ return `
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ `;
+ }
+
+ variableTemplate(variable, index) {
+ return `
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ `;
+ }
+
+ headerTemplate(header, index) {
+ return `
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ `;
+ }
+
+ responseHeaderTemplate(header, index) {
+ return `
+
+ |
+
+ |
+
+
+ |
+
+ `;
+ }
+
+ collectionTemplate(collection, index) {
+ return `
+
+
+
+
+
+
+
+
+
+
+ ${collection.apis.map((api, apiIndex) => this.collectionApiTemplate(api, apiIndex, collection.id)).join('')}
+
+
+
+ `;
+ }
+
+ collectionApiTemplate(api, apiIndex, collectionId) {
+ return `
+
+ ${api.name}
+
+
+
+
+
+ `;
+ }
+
+ init(servers) {
+ if (servers) {
+ this.servers = servers;
+ }
+ this.bindEvents();
+ this.loadCollections();
+ }
+
+ bindEvents() {
+ const events = [
+ {selector: '.btn_add_header', action: this.addHeader.bind(this)},
+ {selector: '.btn_remove_header', action: this.removeHeader.bind(this)},
+ {selector: '.btn_add_query', action: this.addQueryParam.bind(this)},
+ {selector: '.btn_remove_query', action: this.removeQueryParam.bind(this)},
+ {selector: '.btn_add_variables', action: this.addVariable.bind(this)},
+ {selector: '.btn_remove_variable', action: this.removeVariable.bind(this)},
+
+ {selector: '.send', action: this.sendApiRequest.bind(this)},
+ {selector: '.save', action: this.saveAPIRequest.bind(this)},
+ {selector: '.save_collection', action: this.addCollection.bind(this)},
+ {selector: '#cancel-ajax', action: this.cancelAjaxRequest.bind(this)},
+ {selector: '#new_collection', action: this.showNewCollectionModal.bind(this)},
+ {selector: '#new_request', action: this.showNewApiRequestModal.bind(this)},
+ {selector: '.edit_collection', action: this.showCollectionEditModal.bind(this)},
+ {selector: '.delete_collection', action: this.showCollectionDeleteModal.bind(this)},
+ {selector: '.confirm_delete', action: this.removeCollection.bind(this)},
+ {selector: '.add_new_request', action: this.addAPIRequest.bind(this)},
+ {selector: '.edit_api_request', action: this.showEditNameModal.bind(this)},
+ {selector: '.delete_api_request', action: this.showDeleteApiRequestModal.bind(this)},
+ {selector: '.open_api_request', action: this.openApiRequest.bind(this)},
+ {selector: '.confirm_delete_api', action: this.deleteApiRequest.bind(this)},
+ {selector: '.close_tab', action: this.closeTab.bind(this)}
+
+ ];
+
+ for (let event of events) {
+ $(document).on('click', event.selector, event.action);
+ }
+
+ $(document).on('input', '.query_params input', this.handleUIUpdate.bind(this));
+ $(document).on('input', '.query_params input', this.buildPath.bind(this));
+ $(document).on('change', '.query_params input', this.handleUIUpdate.bind(this));
+ $(document).on('change', '.query_params input', this.buildPath.bind(this));
+ $(document).on('input', '.path', this.parseParam.bind(this));
+ $(document).on('change', '.request_headers input', this.handleUIUpdate.bind(this));
+ $(document).on('change', '.variables input', this.handleUIUpdate.bind(this));
+ $(document).on('change', '.api_request_info select, .api_request_info input', this.handleUIUpdate.bind(this));
+ $(document).on('change', '.api_request_info select', this.handleUpdateServer.bind(this));
+
+
+ }
+
+ showCollectionDeleteModal(event) {
+ const collectionId = $(event.currentTarget).closest('button').data('collection');
+ $('#confirm_delete_modal').find('.confirm_delete').attr('data-id', collectionId);
+ $('#confirm_delete_modal').modal('show');
+ }
+
+ showCollectionEditModal(event) {
+ const me = this;
+ const collectionId = $(event.currentTarget).closest('button').data('collection');
+ const collection = this.collections.filter(function (collection) {
+ return collection.id === collectionId;
+ })[0];
+
+ $('#edit_name_modal').find('input').val('');
+ $('#edit_name_modal').modal('show');
+
+ let saveBtn = $('#edit_name_modal').find('button.change_name');
+ saveBtn.off('click');
+
+ saveBtn.on('click', function () {
+ collection.name = $('#edit_name_modal').find('input').val();
+ me.saveCollection(collection.id, collection.name);
+ me.renderCollections();
+ $('#edit_name_modal').modal('hide');
+ });
+ }
+
+ showDeleteApiRequestModal(event) {
+ let collectionId = $(event.currentTarget).closest('li').data('collection');
+ let apiId = $(event.currentTarget).closest('li').data('id');
+
+ $('#confirm_delete_api_modal').find('.confirm_delete_api').attr('data-id', apiId);
+ $('#confirm_delete_api_modal').find('.confirm_delete_api').attr('data-collection', collectionId);
+ $('#confirm_delete_api_modal').modal('show');
+ }
+
+ showNewApiRequestModal(event) {
+ if (this.collections.length == 0) {
+ alert('새로운 컬렉션을 추가해 주세요.');
+ return;
+ }
+ $('#new_api_request_modal').modal('show');
+ }
+
+ showNewCollectionModal(event) {
+ $('#new_collection_modal').find('input').val('');
+ $('#new_collection_modal').modal('show');
+ }
+
+ showEditNameModal(event) {
+ const me = this;
+ const apiId = $(event.currentTarget).closest('li').data('id');
+ const collectionId = $(event.currentTarget).closest('li').data('collection');
+
+ const collection = this.collections.filter(function (collection) {
+ return collection.id === collectionId;
+ })[0];
+
+ let apiRequest = collection.apis.filter(function (api) {
+ return api.id === apiId;
+ })[0];
+
+ apiRequest.collectionId = collectionId;
+
+ let saveBtn = $('#edit_name_modal').find('button.change_name');
+ saveBtn.off('click');
+ saveBtn.on('click', function () {
+ apiRequest.name = $('#edit_name_modal').find('input').val();
+ me.changeAPIName(apiRequest);
+ me.renderCollections();
+ $('#edit_name_modal').modal('hide');
+
+ //탭이 열려 있으면, 닫았다가 다시 열어야 함.
+ me.renderTabHeader();
+ me.renderTabContent(apiRequest.index);
+
+ });
+ $('#edit_name_modal').find('input').val('');
+ $('#edit_name_modal').modal('show');
+ }
+
+ getFieldValue(element) {
+ if ($(element).is('select') || $(element).is('input[type="text"]')) {
+ return $(element).val();
+ }
+ if ($(element).is('input[type="checkbox"]')) {
+ return $(element).is(':checked');
+ }
+ }
+
+ handleUIUpdate(event) {
+ if (event) {
+ event.preventDefault();
+ const element = event.currentTarget;
+ const fieldName = $(element).attr('name');
+ //console.log(fieldName);
+ const newValue = this.getFieldValue(element);
+ //console.log(newValue);
+
+ const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
+ let model = this.apiRequests[tabIndex];
+
+ if (fieldName !== '') {
+ _.set(model, fieldName, newValue);
+ }
+ //console.log(model);
+ }
+ }
+
+ handleUpdateServer(event) {
+ const tabIndex = $(event.currentTarget).closest('.api_request').data('index');
+ let model = this.apiRequests[tabIndex];
+ let target = $('#api_request_' + model.id);
+ //console.log('model.server: ' + model.server);
+ target.find('input[name="basePath"]').val(this.basePath(model.server));
+ }
+
+ renderParams(params) {
+ return params.map((param, index) => this.queryParamTemplate(param, index)).join('');
+ }
+
+ renderVariables(variables) {
+ return variables.map((variable, index) => this.variableTemplate(variable, index)).join('');
+ }
+
+ renderHeaders(headers) {
+ return headers.map((header, index) => this.headerTemplate(header, index)).join('');
+ }
+
+ renderTabHeader() {
+ //console.log('renderTabHeader');
+ let apiRequestTabTitle = $('#apiRequestTabTitle');
+ apiRequestTabTitle.empty();
+
+ for (let idx = 0; idx < this.apiRequests.length; idx++) {
+ let li = `
+
+ `;
+ apiRequestTabTitle.append(li);
+ }
+ }
+
+ renderTabContent(index) {
+ var me = this;
+ //console.log('renderTabContent: ' + index);
+ let apiRequestTabContent = $(this.apiTabContentTarget);
+ let model = this.apiRequests[index];
+
+ apiRequestTabContent.append(this.apiRequestTemplate(model, index));
+
+ this.openTab(model.id);
+ let target = $('#api_request_' + model.id);
+
+ require(['vs/editor/editor.main'], function () {
+ let requestBodyEditor = monaco.editor.create(target.find(".request_body")[0], {
+ value: model.requestBody,
+ language: 'json',
+ theme: 'vs-light',
+ automaticLayout: true
+ });
+ me.requestEditors.push(requestBodyEditor);
+ target.find(".request_body")[0].addEventListener('shown.bs.tab', function () {
+ me.requestEditors[index].layout();
+ });
+
+ me.requestEditors[index].onDidChangeModelContent(function (e) {
+ model.requestBody = requestBodyEditor.getValue();
+ });
+
+ target.find('.request_body_type').on('change', function () {
+ let model = requestBodyEditor.getModel();
+ monaco.editor.setModelLanguage(model, $(this).val());
+ });
+
+ let responseBodyEditor = monaco.editor.create(target.find(".response_body")[0], {
+ value: '',
+ language: 'json',
+ theme: 'vs-light',
+ automaticLayout: true
+ });
+ me.responseEditors.push(responseBodyEditor);
+ });
+ }
+
+ renderResponse(tabIndex) {
+ //console.log('renderResponse: ' + tabIndex);
+ let model = this.apiRequests[tabIndex];
+ let target = $('#api_request_' + model.id);
+ target.find('.response_headers').empty();
+ target.find(".response_status").empty();
+
+ //console.log(model.responseModel);
+
+ try {
+ const parsedJson = JSON.parse(model.responseModel.body);
+ const formattedJson = JSON.stringify(parsedJson, null, 2);
+ this.responseEditors[tabIndex].setValue(formattedJson);
+ } catch(e) {
+ this.responseEditors[tabIndex].setValue(model.responseModel.body);
+ }
+ if(model.responseModel && model.responseModel.headers) {
+ for (let i = 0; i < model.responseModel.headers.length; i++) {
+ target.find('.response_headers').append(this.responseHeaderTemplate(model.responseModel.headers[i], i));
+ }
+ }
+
+ target.find(".response_status").text("status: " + model.responseModel.status);
+ target.find(".response_time").text("time: " + model.responseModel.time + "ms");
+ target.find(".response_size").text("size: " + model.responseModel.size + "bytes");
+
+ }
+
+
+ renderCollections() {
+ let select = $('#modal_collection');
+ let sidemenu = $(this.collectionTarget);
+ select.empty();
+ sidemenu.empty();
+
+ for (let i = 0; i < this.collections.length; i++) {
+ sidemenu.append(this.renderCollection(this.collections[i], i));
+ let option = $('