Netty 서버 상태 추적 및 목업 라우트 관리 기능 개선
This commit is contained in:
@@ -39,3 +39,8 @@ out/
|
||||
ApiTestManager/node_modules
|
||||
ApiTestManager/dist
|
||||
.cache
|
||||
.env
|
||||
CLAUDE.md
|
||||
|
||||
src/main/generated
|
||||
.claude
|
||||
@@ -8,6 +8,7 @@ import com.eactive.testmaster.api.entity.MockRoute;
|
||||
import com.eactive.testmaster.api.mapper.MockRouteMapper;
|
||||
import com.eactive.testmaster.api.repository.MockRouteGroupRepository;
|
||||
import com.eactive.testmaster.api.service.MockRouteService;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.annotation.PostConstruct;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -66,6 +67,7 @@ public class MockRouteMgmtController {
|
||||
mav.addObject("pageable", pageable);
|
||||
mav.addObject("modelSearch", mockRouteSearch);
|
||||
mav.addObject("groupList", mockRouteGroupRepository.findAll());
|
||||
mav.addObject("methodList", Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"));
|
||||
|
||||
return mav;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,16 @@ public class MockRouteSearch implements BaseSearch<MockRoute> {
|
||||
|
||||
private String name;
|
||||
|
||||
private String group;
|
||||
private List<String> group;
|
||||
|
||||
private List<String> method;
|
||||
|
||||
@Override
|
||||
public List<SearchModel> buildSearchCondition() {
|
||||
List<SearchModel> models = new ArrayList<>();
|
||||
models.add(new ColumnSearchModel("name", SearchCondition.LIKE, name));
|
||||
models.add(new ColumnSearchModel("group", SearchCondition.EQUAL, group));
|
||||
models.add(new ColumnSearchModel("group", SearchCondition.IN, group));
|
||||
models.add(new ColumnSearchModel("method", SearchCondition.IN, method));
|
||||
return models;
|
||||
}
|
||||
|
||||
@@ -30,11 +33,19 @@ public class MockRouteSearch implements BaseSearch<MockRoute> {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
public List<String> getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public void setGroup(String group) {
|
||||
public void setGroup(List<String> group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public List<String> getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public void setMethod(List<String> method) {
|
||||
this.method = method;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@ public class Console {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Console.class);
|
||||
|
||||
public void log(String message) {
|
||||
logger.info(message);
|
||||
public void log(Object... args) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (i > 0) sb.append(" ");
|
||||
sb.append(String.valueOf(args[i]));
|
||||
}
|
||||
logger.info(sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import com.eactive.testmaster.loadtest.service.Console;
|
||||
import javax.script.Bindings;
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptContext;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import javax.script.ScriptException;
|
||||
@@ -23,6 +26,9 @@ public class NettySocketHandler extends ChannelInboundHandlerAdapter {
|
||||
ScriptEngineManager manager = new ScriptEngineManager();
|
||||
this.engine = manager.getEngineByName("nashorn");
|
||||
try {
|
||||
Bindings bindings = engine.createBindings();
|
||||
bindings.put("console", new Console());
|
||||
engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
|
||||
engine.eval(scriptContent);
|
||||
} catch (ScriptException e) {
|
||||
logger.error("Failed to initialize script", e);
|
||||
|
||||
+7
@@ -2,7 +2,9 @@ package com.eactive.testmaster.socket.controller;
|
||||
|
||||
import com.eactive.testmaster.socket.entity.NettyServerEntity;
|
||||
import com.eactive.testmaster.socket.service.NettyServerService;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -40,9 +42,14 @@ public class NettyTCPServerMgmtController {
|
||||
Pageable pageable) {
|
||||
ModelAndView mav = new ModelAndView(NETTY_SERVER_LIST_VIEW);
|
||||
Page<NettyServerEntity> page = nettyServerService.findAll(serverSearch.buildSpecification(), pageable);
|
||||
Map<Integer, Boolean> runningStatus = new HashMap<>();
|
||||
for (NettyServerEntity server : page.getContent()) {
|
||||
runningStatus.put(server.getPort(), nettyServerService.isServerRunning(server.getPort()));
|
||||
}
|
||||
mav.addObject("page", page);
|
||||
mav.addObject("pageable", pageable);
|
||||
mav.addObject("modelSearch", serverSearch);
|
||||
mav.addObject("runningStatus", runningStatus);
|
||||
return mav;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,11 @@ public class NettyServerService {
|
||||
existingServer.setPort(updatedServer.getPort());
|
||||
existingServer.setActive(updatedServer.getActive());
|
||||
existingServer.setName(updatedServer.getName());
|
||||
existingServer.setScript(updatedServer.getScript()); // Add this line to update the script
|
||||
existingServer.setScript(updatedServer.getScript());
|
||||
existingServer.setPrefixSize(updatedServer.getPrefixSize());
|
||||
existingServer.setIncludePrefixLength(updatedServer.getIncludePrefixLength());
|
||||
existingServer.setServiceIdLength(updatedServer.getServiceIdLength());
|
||||
existingServer.setServiceIdPosition(updatedServer.getServiceIdPosition());
|
||||
|
||||
return repository.save(existingServer);
|
||||
}
|
||||
@@ -116,10 +120,9 @@ public class NettyServerService {
|
||||
// return repository.findByActive(true);
|
||||
// }
|
||||
|
||||
// @Transactional(readOnly = true)
|
||||
// public boolean isServerRunning(Integer port) {
|
||||
// return activeServers.containsKey(port);
|
||||
// }
|
||||
public boolean isServerRunning(Integer port) {
|
||||
return activeServers.containsKey(port);
|
||||
}
|
||||
|
||||
// @Transactional(readOnly = true)
|
||||
// public Map<Integer, Boolean> getServerStatuses(List<Integer> ports) {
|
||||
|
||||
@@ -14,25 +14,66 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<form role="form" class="row g-3" name="searchForm" th:action="@{/mgmt/routes/list_view.do}" method="get" th:object="${modelSearch}">
|
||||
<form role="form" class="row g-3 align-items-center" name="searchForm" th:action="@{/mgmt/routes/list_view.do}" method="get" th:object="${modelSearch}">
|
||||
<input name="page" type="hidden" value="1"/>
|
||||
<div class="col-auto">
|
||||
<select class="form-select" name="group" th:value="${modelSearch.group}">
|
||||
<option value="" th:selected="${#strings.isEmpty(modelSearch.group)}">선택</option>
|
||||
<input name="size" type="hidden" th:value="${pageable.pageSize}"/>
|
||||
<input name="sort" type="hidden" th:value="${#strings.defaultString(param.sort, 'id,desc')}"/>
|
||||
|
||||
<!-- 그룹 목록 반복 -->
|
||||
<option th:each="group : ${groupList}"
|
||||
th:value="${group.id}"
|
||||
th:text="${group.name}"
|
||||
th:selected="${modelSearch.group} == ${group.id}">
|
||||
</option>
|
||||
</select>
|
||||
<!-- Group 복수 선택 드롭다운 -->
|
||||
<div class="col-auto">
|
||||
<div class="dropdown multi-select-dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" id="groupDropdownBtn">
|
||||
Group
|
||||
</button>
|
||||
<ul class="dropdown-menu p-2" style="min-width: 200px;">
|
||||
<li th:each="group : ${groupList}">
|
||||
<label class="dropdown-item d-flex align-items-center" style="cursor: pointer;">
|
||||
<input type="checkbox" class="form-check-input me-2 group-check"
|
||||
th:value="${group.id}"
|
||||
th:checked="${modelSearch.group != null AND #lists.contains(modelSearch.group, '' + group.id)}"/>
|
||||
<span th:text="${group.name}"></span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Method 복수 선택 드롭다운 -->
|
||||
<div class="col-auto">
|
||||
<div class="dropdown multi-select-dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" id="methodDropdownBtn">
|
||||
Method
|
||||
</button>
|
||||
<ul class="dropdown-menu p-2" style="min-width: 180px;">
|
||||
<li th:each="m : ${methodList}">
|
||||
<label class="dropdown-item d-flex align-items-center" style="cursor: pointer;">
|
||||
<input type="checkbox" class="form-check-input me-2 method-check"
|
||||
th:value="${m}"
|
||||
th:checked="${modelSearch.method != null AND #lists.contains(modelSearch.method, m)}"/>
|
||||
<span th:text="${m}"></span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<input class="form-control" name="name" placeholder="이름" type="text" size="35" th:title="#{title.search}+ '' + #{input.input}"
|
||||
th:value="${modelSearch.name}"
|
||||
maxlength="255"/>
|
||||
</div>
|
||||
|
||||
<!-- 정렬 드롭다운 -->
|
||||
<div class="col-auto">
|
||||
<select class="form-select" id="sortSelect">
|
||||
<option value="id,desc" th:selected="${#strings.defaultString(param.sort, 'id,desc') == 'id,desc'}">최신순</option>
|
||||
<option value="group,asc" th:selected="${param.sort == 'group,asc'}">Group</option>
|
||||
<option value="method,asc" th:selected="${param.sort == 'method,asc'}">Method</option>
|
||||
<option value="pathPattern,asc" th:selected="${param.sort == 'pathPattern,asc'}">Path</option>
|
||||
<option value="name,asc" th:selected="${param.sort == 'name,asc'}">이름</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-primary btn_search" th:value="#{button.inquire}"><i class="fa-sharp fa-solid fa-magnifying-glass"></i> [[#{title.inquire}]]
|
||||
</button>
|
||||
@@ -41,10 +82,10 @@
|
||||
<button type="button" class="btn btn-primary btn_delete"><i class="fa-sharp fa-solid fa-trash"></i> [[#{title.delete}]]</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-primary btn_add"><i class="fa-sharp fa-solid fa-plus"></i> [[#{button.create}]]</button><!-- 등록 -->
|
||||
<button type="button" class="btn btn-primary btn_add"><i class="fa-sharp fa-solid fa-plus"></i> [[#{button.create}]]</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-primary btn_add_group"><i class="fa-sharp fa-solid fa-plus"></i>그룹 관리</button><!-- 등록 -->
|
||||
<button type="button" class="btn btn-primary btn_add_group"><i class="fa-sharp fa-solid fa-plus"></i>그룹 관리</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -123,7 +164,7 @@
|
||||
</tr>
|
||||
<!-- 글이 없는 경우 -->
|
||||
<tr th:if="${page.content.size() == 0 }">
|
||||
<td colspan="6" th:text="#{common.nodata.msg}"></td>
|
||||
<td colspan="7" th:text="#{common.nodata.msg}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -133,7 +174,17 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div th:replace="~{fragment/pagination :: pagination(jsFunction='fn_select_page')}"></div>
|
||||
<div class="d-flex align-items-center">
|
||||
<label class="me-2" style="white-space: nowrap;">페이지당:</label>
|
||||
<select class="form-select form-select-sm" id="pageSizeSelect" style="width: auto;">
|
||||
<option value="20" th:selected="${pageable.pageSize == 20}">20</option>
|
||||
<option value="50" th:selected="${pageable.pageSize == 50}">50</option>
|
||||
<option value="100" th:selected="${pageable.pageSize == 100}">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,9 +196,12 @@
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script>
|
||||
function fn_select_page(pageNo) {
|
||||
function fn_select_page(pageNo, size) {
|
||||
document.searchForm.page.value = pageNo;
|
||||
document.searchForm.submit();
|
||||
if (size) {
|
||||
document.searchForm.size.value = size;
|
||||
}
|
||||
syncFiltersAndSubmit();
|
||||
}
|
||||
|
||||
function fnSelectItem(id) {
|
||||
@@ -159,17 +213,91 @@
|
||||
document.cloneForm.submit();
|
||||
}
|
||||
|
||||
$(function () {
|
||||
let fnSearch = document.querySelector(".btn_search");
|
||||
let fnDelete = document.querySelector(".btn_delete");
|
||||
let fnAdd = document.querySelector(".btn_add");
|
||||
let fnAddGroup = document.querySelector(".btn_add_group");
|
||||
function updateDropdownLabel(checkClass, btnId, defaultLabel) {
|
||||
var checks = document.querySelectorAll('.' + checkClass + ':checked');
|
||||
var btn = document.getElementById(btnId);
|
||||
if (checks.length > 0) {
|
||||
btn.textContent = defaultLabel + ' (' + checks.length + ')';
|
||||
} else {
|
||||
btn.textContent = defaultLabel;
|
||||
}
|
||||
}
|
||||
|
||||
fnSearch.addEventListener("click", function () {
|
||||
document.searchForm.page.value = 1;
|
||||
document.searchForm.submit();
|
||||
function syncFiltersAndSubmit() {
|
||||
var form = document.searchForm;
|
||||
|
||||
// 기존 group, method hidden input 제거
|
||||
var oldGroups = form.querySelectorAll('input[name="group"]');
|
||||
oldGroups.forEach(function(el) { el.remove(); });
|
||||
var oldMethods = form.querySelectorAll('input[name="method"]');
|
||||
oldMethods.forEach(function(el) { el.remove(); });
|
||||
|
||||
// 선택된 group 값 추가
|
||||
var groupChecks = document.querySelectorAll('.group-check:checked');
|
||||
groupChecks.forEach(function(cb) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'group';
|
||||
input.value = cb.value;
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
// 선택된 method 값 추가
|
||||
var methodChecks = document.querySelectorAll('.method-check:checked');
|
||||
methodChecks.forEach(function(cb) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'method';
|
||||
input.value = cb.value;
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
// sort 값 동기화
|
||||
form.sort.value = document.getElementById('sortSelect').value;
|
||||
|
||||
form.submit();
|
||||
}
|
||||
|
||||
$(function () {
|
||||
var fnSearch = document.querySelector(".btn_search");
|
||||
var fnDelete = document.querySelector(".btn_delete");
|
||||
var fnAdd = document.querySelector(".btn_add");
|
||||
var fnAddGroup = document.querySelector(".btn_add_group");
|
||||
|
||||
// localStorage에서 페이지 크기 복원
|
||||
var savedPageSize = localStorage.getItem('mockRouteListPageSize');
|
||||
if (savedPageSize) {
|
||||
var pageSizeSelect = document.getElementById('pageSizeSelect');
|
||||
pageSizeSelect.value = savedPageSize;
|
||||
// 현재 서버 페이지 크기와 다르면 반영
|
||||
if (document.searchForm.size.value !== savedPageSize) {
|
||||
document.searchForm.size.value = savedPageSize;
|
||||
}
|
||||
}
|
||||
|
||||
// 드롭다운 라벨 초기화
|
||||
updateDropdownLabel('group-check', 'groupDropdownBtn', 'Group');
|
||||
updateDropdownLabel('method-check', 'methodDropdownBtn', 'Method');
|
||||
|
||||
// 체크박스 변경 시 라벨 업데이트
|
||||
document.querySelectorAll('.group-check').forEach(function(cb) {
|
||||
cb.addEventListener('change', function() {
|
||||
updateDropdownLabel('group-check', 'groupDropdownBtn', 'Group');
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.method-check').forEach(function(cb) {
|
||||
cb.addEventListener('change', function() {
|
||||
updateDropdownLabel('method-check', 'methodDropdownBtn', 'Method');
|
||||
});
|
||||
});
|
||||
|
||||
// 조회 버튼
|
||||
fnSearch.addEventListener("click", function () {
|
||||
document.searchForm.page.value = 1;
|
||||
syncFiltersAndSubmit();
|
||||
});
|
||||
|
||||
// 삭제 버튼
|
||||
fnDelete.addEventListener("click", function () {
|
||||
var checkField = document.listForm.checkField;
|
||||
var id = document.listForm.checkId;
|
||||
@@ -206,6 +334,21 @@
|
||||
fnAddGroup.addEventListener("click", function () {
|
||||
location.href = '[[@{/mgmt/routes/group/list_view.do}]]';
|
||||
});
|
||||
|
||||
// 정렬 변경 시 자동 submit
|
||||
document.getElementById('sortSelect').addEventListener('change', function() {
|
||||
document.searchForm.page.value = 1;
|
||||
syncFiltersAndSubmit();
|
||||
});
|
||||
|
||||
// 페이지 크기 변경
|
||||
document.getElementById('pageSizeSelect').addEventListener('change', function() {
|
||||
var newSize = this.value;
|
||||
localStorage.setItem('mockRouteListPageSize', newSize);
|
||||
document.searchForm.size.value = newSize;
|
||||
document.searchForm.page.value = 1;
|
||||
syncFiltersAndSubmit();
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -59,6 +59,46 @@
|
||||
<label class="form-check-label" for="active">Active</label>
|
||||
</div>
|
||||
|
||||
<!-- Message Format Settings -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Message Format</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="form-floating">
|
||||
<input type="number" class="form-control"
|
||||
th:field="*{prefixSize}" min="0" max="20"/>
|
||||
<label for="prefixSize" class="form-label">Prefix Size</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check mt-3">
|
||||
<input type="checkbox" class="form-check-input"
|
||||
th:field="*{includePrefixLength}" id="includePrefixLength"/>
|
||||
<label class="form-check-label" for="includePrefixLength">Include Prefix in Length</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-floating">
|
||||
<input type="number" class="form-control"
|
||||
th:field="*{serviceIdLength}" min="0" max="100"/>
|
||||
<label for="serviceIdLength" class="form-label">Service ID Length</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-floating">
|
||||
<select class="form-select" th:field="*{serviceIdPosition}">
|
||||
<option value="NONE">None</option>
|
||||
<option value="START">Start</option>
|
||||
<option value="END">End</option>
|
||||
</select>
|
||||
<label for="serviceIdPosition" class="form-label">Service ID Position</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script Editor -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">Server Script</div>
|
||||
|
||||
@@ -92,6 +92,8 @@
|
||||
<td class="text-center">
|
||||
<span th:class="${row.active ? 'badge bg-success' : 'badge bg-danger'}"
|
||||
th:text="${row.active ? 'Active' : 'Inactive'}"></span>
|
||||
<span th:if="${runningStatus[row.port]}" class="badge bg-primary">Started</span>
|
||||
<span th:unless="${runningStatus[row.port]}" class="badge bg-secondary">Stopped</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span th:text="${#temporals.format(row.lastModified, 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
|
||||
Reference in New Issue
Block a user