Netty 서버 상태 추적 및 목업 라우트 관리 기능 개선

This commit is contained in:
Rinjae
2026-02-24 17:37:05 +09:00
parent 9d54f81c0e
commit 51a8cb5327
10 changed files with 260 additions and 36 deletions
+5
View File
@@ -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);
@@ -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}"
<!-- 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:text="${group.name}"
th:selected="${modelSearch.group} == ${group.id}">
</option>
</select>
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 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>