fix: dap-was-dapmt 바라보도록 수정

This commit is contained in:
juheelee
2026-08-20 14:58:50 +09:00
parent 4986f9801c
commit 52ae31d853
30 changed files with 495 additions and 359 deletions

View File

@@ -1,51 +1,45 @@
package io.shinhanlife.dat.mcg.presentation;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.server.ResponseStatusException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Gateway 내부 MCP 목록 조회와 Report 서비스의 Excel API 프록시를 담당한다. */
/** Tool Report 화면과 소스 분석 서비스를 연결한다. */
@RestController
public class ToolReportProxyController {
private final RestClient restClient;
private final String reportServiceUrl;
private final McpRouterController mcpRouterController;
public ToolReportProxyController(
RestClient.Builder restClientBuilder,
McpRouterController mcpRouterController,
@Value("${report.service-url}") String reportServiceUrl) {
public ToolReportProxyController(RestClient.Builder restClientBuilder,
@Value("${report.service-url}") String reportServiceUrl) {
this.restClient = restClientBuilder.build();
this.mcpRouterController = mcpRouterController;
this.reportServiceUrl = reportServiceUrl.replaceAll("/+$", "");
}
/** Tool 목록은 Report 서비스나 로컬 소스가 아니라 Gateway의 MCP Registry에서만 조회한다. */
@GetMapping(value = "/report/api/report-tools", produces = MediaType.APPLICATION_JSON_VALUE)
public List<ReportToolSummary> tools() {
return activeTools().stream().map(ReportToolSummary::from).toList();
public List<ReportToolSummary> tools(@RequestParam(required = false) String sourceType) {
ReportToolSummary[] tools = restClient.get()
.uri(reportServiceUrl + "/api/report-tools?sourceType={sourceType}",
sourceType == null ? "DAP_WAS_DAPMT" : sourceType)
.retrieve()
.body(ReportToolSummary[].class);
return tools == null ? List.of() : List.of(tools);
}
@PostMapping(value = "/report/api/tool-reports/excel",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public ResponseEntity<byte[]> excel(@RequestBody Map<String, Object> request) {
validateMcpSelection(request);
ResponseEntity<byte[]> response = restClient.post()
.uri(reportServiceUrl + "/api/tool-reports/excel")
.contentType(MediaType.APPLICATION_JSON)
@@ -55,28 +49,6 @@ public class ToolReportProxyController {
return copy(response);
}
private void validateMcpSelection(Map<String, Object> request) {
Object rawNames = request.get("toolNames");
if (!(rawNames instanceof List<?> names) || names.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "toolNames must contain at least one tool");
}
Set<String> registeredNames = activeTools().stream()
.map(ToolMetadata::getName)
.collect(java.util.stream.Collectors.toSet());
Set<String> unknown = new LinkedHashSet<>();
names.stream().map(String::valueOf)
.filter(name -> !registeredNames.contains(name))
.forEach(unknown::add);
if (!unknown.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"MCP에 등록되지 않은 툴이 포함되어 있습니다: " + unknown);
}
}
private List<ToolMetadata> activeTools() {
return mcpRouterController.activeToolsForReport();
}
private ResponseEntity<byte[]> copy(ResponseEntity<byte[]> response) {
ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode());
MediaType contentType = response.getHeaders().getContentType();
@@ -87,38 +59,12 @@ public class ToolReportProxyController {
}
public record ReportToolSummary(
String name,
String title,
String description,
String categoryKey,
String mappingId,
boolean register,
boolean requiresApproval,
boolean readOnlyHint,
boolean destructiveHint,
boolean idempotentHint,
boolean openWorldHint,
String requestType,
String responseType,
String useCaseClass,
String sourceFile,
String inputSchemaResource,
String outputSchemaResource) {
static ReportToolSummary from(ToolMetadata tool) {
String name = tool.getName() == null || tool.getName().isBlank() ? tool.getUid() : tool.getName();
String title = tool.getDisplayName() == null || tool.getDisplayName().isBlank()
? name : tool.getDisplayName();
return new ReportToolSummary(
name, title, value(tool.getDescription()), value(tool.getCategoryKey()),
value(tool.getMciServiceId()), Boolean.TRUE.equals(tool.getIsRegistered()),
Boolean.TRUE.equals(tool.getRequiresApproval()), Boolean.TRUE.equals(tool.getReadOnlyHint()),
Boolean.TRUE.equals(tool.getDestructiveHint()), Boolean.TRUE.equals(tool.getIdempotentHint()),
Boolean.TRUE.equals(tool.getOpenWorldHint()), "", "", "", "", "", "");
}
private static String value(String value) {
return value == null ? "" : value;
}
String name, String title, String description, String categoryKey, String mappingId,
boolean register, boolean requiresApproval, boolean readOnlyHint, boolean destructiveHint,
boolean idempotentHint, boolean openWorldHint, String requestType, String responseType,
String useCaseClass, String sourceFile, String inputSchemaResource, String outputSchemaResource,
String version, String functionDescription, String whenToUse, String whenNotToUse,
String ioLimits, String displayDescription, String exampleQueries, String tags,
String requiredEnvKeys, String ownerOrg, String definitionFile) {
}
}

View File

@@ -31,7 +31,7 @@
.subtitle { margin: 0; color: #a1a1aa; font-size: 14px; line-height: 1.65; }
.count-badge { padding: 7px 11px; border: 1px solid #27272a; border-radius: 8px; background: #18181b; color: #a1a1aa; font-family: "Geist Mono", Consolas, monospace; font-size: 12px; white-space: nowrap; }
.panel { overflow: hidden; border: 1px solid #27272a; border-radius: 12px; background: #111113; box-shadow: 0 18px 50px rgba(0,0,0,.22); }
.toolbar { display: grid; grid-template-columns: minmax(260px,1fr) 190px auto auto; gap: 10px; padding: 16px; border-bottom: 1px solid #27272a; background: #111113; }
.toolbar { display: grid; grid-template-columns: 210px minmax(240px,1fr) 170px auto auto; gap: 10px; padding: 16px; border-bottom: 1px solid #27272a; background: #111113; }
input, select, button { min-height: 40px; border: 1px solid #3f3f46; border-radius: 7px; background: #18181b; color: #e4e4e7; padding: 8px 12px; font: inherit; font-size: 13px; outline: none; }
input::placeholder { color: #71717a; }
input:focus, select:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,.12); }
@@ -95,6 +95,10 @@
<section class="panel">
<div class="toolbar">
<select id="sourceType" aria-label="Tool 소스 프로젝트">
<option value="DAP_WAS_DAPMT">dap-was-dapmt 소스</option>
<option value="DAP_ADMIN">dap-admin 소스</option>
</select>
<input id="keyword" type="search" autocomplete="off" placeholder="툴명, 제목 또는 설명 검색">
<select id="category"><option value="">전체 카테고리</option></select>
<button id="selectAll" type="button">전체 선택</button>
@@ -114,7 +118,7 @@
</main>
<script>
const state = { tools: [], selected: new Set() };
const state = { tools: [], selected: new Set(), sourceType: 'DAP_WAS_DAPMT' };
const el = id => document.getElementById(id);
const gateway = location.origin;
@@ -155,7 +159,11 @@
async function load() {
try {
const response = await fetch('/report/api/report-tools');
state.sourceType = el('sourceType').value;
state.selected.clear();
state.tools = [];
el('tools').innerHTML = '<tr><td class="loading-row" colspan="6">선택한 프로젝트의 Tool 소스를 분석하고 있습니다.</td></tr>';
const response = await fetch(`/report/api/report-tools?sourceType=${encodeURIComponent(state.sourceType)}`);
const body = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(body?.error || body?.message || `툴 목록 조회에 실패했습니다. (${response.status})`);
@@ -178,6 +186,10 @@
}
el('keyword').addEventListener('input', render);
el('sourceType').addEventListener('change', () => {
el('category').innerHTML = '<option value="">전체 카테고리</option>';
load();
});
el('category').addEventListener('change', render);
el('selectAll').addEventListener('click', () => { filtered().forEach(tool => state.selected.add(tool.name)); render(); });
el('clearAll').addEventListener('click', () => { filtered().forEach(tool => state.selected.delete(tool.name)); render(); });
@@ -185,7 +197,8 @@
el('download').disabled = true; updateStatus('소스를 분석하고 Excel을 생성하는 중입니다.');
try {
const response = await fetch('/report/api/tool-reports/excel', {
method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({toolNames: [...state.selected]})
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({toolNames: [...state.selected], sourceType: state.sourceType})
});
if (!response.ok) {
const body = await response.json().catch(() => ({}));