refactor: apply new Naming standard (uid, categoryKey, subToolName) across modules

This commit is contained in:
jade
2026-07-11 00:37:29 +09:00
parent 406f093044
commit b9aa8e0169
25 changed files with 135 additions and 117 deletions

View File

@@ -36,7 +36,10 @@ import java.util.Map;
public class ToolMetadata {
// 1. Tool 기본 정보
private String toolName; // 툴 고유 명칭 (예: CustomerSearchTool)
private String uid; // UUID 형식의 고유 식별자
private String semver; // 버전 (예: 1.0.0)
private String name; // 사람이 읽는 라벨 (1-128자)
private String subToolName; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
@@ -45,8 +48,8 @@ public class ToolMetadata {
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
private Map<String, String> actionPrompts;
// 2-1. 도메인 부서 그룹명 (권한 관리에 사용)
private String domainGroup;
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
private String categoryKey;
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
private String endpoint;

View File

@@ -109,11 +109,11 @@ public class McpRouterController {
public ResponseEntity<JsonRpcResponse> listTools() {
List<ToolMetadata> activeTools = redisRegistryService.getAllTools()
.stream()
.filter(ToolMetadata::isVisible)
.filter(ToolMetadata::getVisible)
.collect(java.util.stream.Collectors.toList());
java.util.Set<String> knownTools = activeTools.stream()
.map(ToolMetadata::getToolName)
.map(ToolMetadata::getUid)
.collect(java.util.stream.Collectors.toSet());
java.util.Set<String> fallbackUrls = new java.util.HashSet<>(gatewayFallbackProperties.getRoutes().values());
@@ -130,9 +130,9 @@ public class McpRouterController {
if (localTools != null) {
for (ToolMetadata t : localTools) {
if (!Boolean.TRUE.equals(t.getIsRegistered()) && Boolean.TRUE.equals(t.getVisible()) && !knownTools.contains(t.getToolName())) {
if (!Boolean.TRUE.equals(t.getIsRegistered()) && Boolean.TRUE.equals(t.getVisible()) && !knownTools.contains(t.getUid())) {
activeTools.add(t);
knownTools.add(t.getToolName());
knownTools.add(t.getUid());
}
}
}
@@ -152,7 +152,7 @@ public class McpRouterController {
public ResponseEntity<String> generateToolsMarkdown() {
List<ToolMetadata> tools = redisRegistryService.getAllTools()
.stream()
.filter(ToolMetadata::isVisible)
.filter(ToolMetadata::getVisible)
.collect(java.util.stream.Collectors.toList());
StringBuilder md = new StringBuilder();
@@ -165,7 +165,7 @@ public class McpRouterController {
// Group by Domain Group
Map<String, List<ToolMetadata>> groupedTools = new java.util.HashMap<>();
for (ToolMetadata tool : tools) {
String group = tool.getDomainGroup() != null ? tool.getDomainGroup() : "기타 (Others)";
String group = tool.getCategoryKey() != null ? tool.getCategoryKey() : "기타 (Others)";
groupedTools.computeIfAbsent(group, k -> new java.util.ArrayList<>()).add(tool);
}
@@ -174,7 +174,7 @@ public class McpRouterController {
int index = 1;
for (ToolMetadata tool : entry.getValue()) {
md.append("### ").append(index++).append(". ").append(tool.getToolName()).append("\n");
md.append("### ").append(index++).append(". ").append(tool.getUid()).append("\n");
if (tool.getDescription() != null) {
md.append("- **설명**: ").append(tool.getDescription()).append("\n");
}
@@ -223,14 +223,14 @@ public class McpRouterController {
}
@PostMapping("/registry/deregister")
public ResponseEntity<String> deregisterTool(@RequestBody String toolName) {
redisRegistryService.removeTool(toolName);
public ResponseEntity<String> deregisterTool(@RequestBody String uid) {
redisRegistryService.removeTool(uid);
return ResponseEntity.ok("Deregistered");
}
@PostMapping("/registry/heartbeat")
public ResponseEntity<String> heartbeat(@RequestBody String toolName) {
boolean success = redisRegistryService.refreshHeartbeat(toolName);
public ResponseEntity<String> heartbeat(@RequestBody String uid) {
boolean success = redisRegistryService.refreshHeartbeat(uid);
if (success) {
return ResponseEntity.ok("Heartbeat updated");
} else {

View File

@@ -39,28 +39,28 @@ public class RedisRegistryService {
* 툴 등록 및 갱신 (TTL 기반으로 60초 뒤 자동 만료)
*/
public void saveTool(ToolMetadata meta) {
String key = KEY_PREFIX + meta.getToolName();
String key = KEY_PREFIX + meta.getUid();
meta.setLastHeartbeat(System.currentTimeMillis());
redisTemplate.opsForValue().set(key, meta, DEFAULT_TTL);
log.info(" [RedisRegistry] 툴 등록 완료: {}", meta.getToolName());
log.info(" [RedisRegistry] 툴 등록 완료: {}", meta.getUid());
}
/**
* 하트비트 갱신 (TTL 초기화)
*/
public boolean refreshHeartbeat(String toolName) {
String key = KEY_PREFIX + toolName;
public boolean refreshHeartbeat(String uid) {
String key = KEY_PREFIX + uid;
Boolean exists = redisTemplate.expire(key, DEFAULT_TTL);
if (Boolean.TRUE.equals(exists)) {
log.debug(" [RedisRegistry] 하트비트 갱신: {}", toolName);
log.debug(" [RedisRegistry] 하트비트 갱신: {}", uid);
return true;
} else {
log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", toolName);
log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", uid);
return false;
}
}
public List<String> getAvailablePods(String toolName) {
ToolMetadata tool = getTool(toolName);
public List<String> getAvailablePods(String uid) {
ToolMetadata tool = getTool(uid);
if (tool != null && tool.getPodUrl() != null) {
return List.of(tool.getPodUrl());
@@ -71,8 +71,8 @@ public class RedisRegistryService {
/**
* 실행 시 툴 정보 조회 (Tool Execution 시 참조)
*/
public ToolMetadata getTool(String toolName) {
return redisTemplate.opsForValue().get(KEY_PREFIX + toolName);
public ToolMetadata getTool(String uid) {
return redisTemplate.opsForValue().get(KEY_PREFIX + uid);
}
/**
@@ -90,8 +90,8 @@ public class RedisRegistryService {
/**
* 툴 명시적 제거 (Deregister)
*/
public void removeTool(String toolName) {
redisTemplate.delete(KEY_PREFIX + toolName);
log.info(" [RedisRegistry] 툴 삭제 완료: {}", toolName);
public void removeTool(String uid) {
redisTemplate.delete(KEY_PREFIX + uid);
log.info(" [RedisRegistry] 툴 삭제 완료: {}", uid);
}
}

View File

@@ -73,19 +73,19 @@ public class ToolPlanner {
log.info(" [Planner] Fallback 라우팅 매칭됨: {} -> {}", toolName, fallbackPodUrl);
toolMetadata = ToolMetadata.builder()
.toolName(toolName)
.uid(toolName)
.integrationType("DIRECT")
.podUrl(fallbackPodUrl)
.build();
}
// 2-1. [신규] 도메인 그룹핑 기반 권한 검증
if (tenantId != null && toolMetadata.getDomainGroup() != null) {
if (tenantId != null && toolMetadata.getCategoryKey() != null) {
List<String> allowedDomains = securityProperties.getTenantDomains().get(tenantId);
if (allowedDomains == null ||
(!allowedDomains.contains("ALL") && !allowedDomains.contains(toolMetadata.getDomainGroup()))) {
log.warn(" [Planner] 권한 거부 - Tenant: {}, Request Domain: {}", tenantId, toolMetadata.getDomainGroup());
throw new SecurityException("해당 도메인(" + toolMetadata.getDomainGroup() + ")의 툴을 실행할 권한이 없습니다.");
(!allowedDomains.contains("ALL") && !allowedDomains.contains(toolMetadata.getCategoryKey()))) {
log.warn(" [Planner] 권한 거부 - Tenant: {}, Request Domain: {}", tenantId, toolMetadata.getCategoryKey());
throw new SecurityException("해당 도메인(" + toolMetadata.getCategoryKey() + ")의 툴을 실행할 권한이 없습니다.");
}
}

View File

@@ -283,7 +283,7 @@
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">소속 도메인 그룹</label>
<input type="text" class="form-control" name="group" list="groupList" placeholder="선택 또는 직접 입력 (대문자)" oninput="this.value = this.value.toUpperCase()" required>
<input type="text" class="form-control" name="categoryKey" pattern="^[a-z0-9][a-z0-9_-]*$" list="groupList" placeholder="선택 또는 직접 입력 (대문자)" oninput="this.value = this.value.toUpperCase()" required>
<datalist id="groupList">
<option value="COMMON">COMMON (공통)</option>
<option value="CUSTOMER">CUSTOMER (고객)</option>
@@ -353,7 +353,7 @@
<thead class="table-light">
<tr>
<th style="width: 10%; min-width: 100px;">그룹</th>
<th style="width: 20%; min-width: 150px;">Tool 이름</th>
<th style="width: 20%; min-width: 150px;">서브툴 이름</th>
<th style="width: 35%; min-width: 200px;">설명</th>
<th style="width: 15%; min-width: 180px;">Pod 모듈</th>
<th style="width: 20%; min-width: 150px;">인터페이스 (ID)</th>
@@ -386,12 +386,12 @@
<div class="modal-body">
<form id="editToolForm">
<div class="mb-3">
<label class="form-label">Tool 이름 (수정불가)</label>
<input type="text" class="form-control" id="editToolName" name="toolName" readonly>
<label class="form-label">서브툴 이름 (수정불가)</label>
<input type="text" class="form-control" id="editSubToolName" name="subToolName" readonly>
</div>
<div class="mb-3">
<label class="form-label">소속 도메인 그룹</label>
<input type="text" class="form-control" id="editDomainGroup" name="domainGroup" oninput="this.value = this.value.toUpperCase()">
<input type="text" class="form-control" id="editCategoryKey" name="categoryKey" pattern="^[a-z0-9][a-z0-9_-]*$" oninput="this.value = this.value.toUpperCase()">
</div>
<div class="mb-3">
<label class="form-label">설명</label>
@@ -485,14 +485,14 @@
// 그룹명 기준으로 오름차순 정렬
tools.sort((a, b) => {
const gA = a.domainGroup || '';
const gB = b.domainGroup || '';
const gA = a.categoryKey || '';
const gB = b.categoryKey || '';
return gA.localeCompare(gB);
});
// 그룹 필터 옵션 채우기
const groupSet = new Set();
tools.forEach(t => groupSet.add(t.domainGroup || 'UNKNOWN'));
tools.forEach(t => groupSet.add(t.categoryKey || 'UNKNOWN'));
const groupSelect = document.getElementById('groupFilterSelect');
const currentSelectedGroup = groupSelect.value;
groupSelect.innerHTML = '<option value="ALL">ALL (전체 그룹)</option>';
@@ -513,10 +513,10 @@
return `
<tr class="tool-row">
<td class="text-nowrap"><span class="badge bg-secondary tool-group">${t.domainGroup || 'UNKNOWN'}</span></td>
<td class="text-nowrap"><span class="badge bg-secondary tool-group">${t.categoryKey || 'UNKNOWN'}</span></td>
<td class="fw-bold text-primary text-break tool-name">
<a href="javascript:void(0)" onclick="openEditModal('${t.toolName}', '${t.domainGroup}', '${escDesc}', ${isReg})" class="text-decoration-none">
${t.toolName}
<a href="javascript:void(0)" onclick="openEditModal('${t.subToolName}', '${t.categoryKey}', '${escDesc}', ${isReg})" class="text-decoration-none">
${t.subToolName}
</a>
${badgeHtml}
</td>
@@ -555,9 +555,9 @@
}
let editModalInstance = null;
function openEditModal(toolName, domainGroup, description, isReg) {
document.getElementById('editToolName').value = toolName;
document.getElementById('editDomainGroup').value = domainGroup !== 'null' ? domainGroup : '';
function openEditModal(subToolName, categoryKey, description, isReg) {
document.getElementById('editSubToolName').value = subToolName;
document.getElementById('editCategoryKey').value = categoryKey !== 'null' ? categoryKey : '';
document.getElementById('editDescription').value = description;
document.getElementById('editRegister').value = isReg ? 'true' : 'false';
@@ -568,8 +568,8 @@
}
function submitToolUpdate() {
const toolName = document.getElementById('editToolName').value;
const domainGroup = document.getElementById('editDomainGroup').value;
const subToolName = document.getElementById('editSubToolName').value;
const categoryKey = document.getElementById('editCategoryKey').value;
const description = document.getElementById('editDescription').value;
const register = document.getElementById('editRegister').value;
@@ -582,8 +582,8 @@
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
toolName: toolName,
domainGroup: domainGroup,
subToolName: subToolName,
categoryKey: categoryKey,
description: description,
register: register
})

View File

@@ -218,7 +218,7 @@
// Group by domain
const grouped = {};
tools.forEach(tool => {
const group = tool.domainGroup || '기타 (Others)';
const group = tool.categoryKey || '기타 (Others)';
if (!grouped[group]) grouped[group] = [];
grouped[group].push(tool);
});
@@ -317,7 +317,7 @@
<div class="absolute top-4 right-4 px-2 py-0.5 rounded text-[9px] font-bold font-mono border tracking-wider ${typeClass}">
${tool.integrationType || 'DIRECT'}
</div>
<h3 class="text-lg font-bold text-white mb-1 pr-16">${tool.toolName}</h3>
<h3 class="text-lg font-bold text-white mb-1 pr-16">${tool.subToolName}</h3>
<p class="text-sm text-slate-400 leading-relaxed min-h-[40px]">${tool.description || '설명이 없습니다.'}</p>
${paramHtml}