This commit is contained in:
@@ -154,6 +154,11 @@ public class McpRouterController {
|
||||
return activeTools;
|
||||
}
|
||||
|
||||
/** Report 화면처럼 내부 메타데이터 전체가 필요한 Gateway 컴포넌트에 활성 Tool 목록을 제공합니다. */
|
||||
List<ToolMetadata> activeToolsForReport() {
|
||||
return fetchAllActiveTools();
|
||||
}
|
||||
|
||||
@GetMapping("/tools/list")
|
||||
public ResponseEntity<JsonRpcResponse> listTools(
|
||||
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
|
||||
@@ -280,4 +285,4 @@ public class McpRouterController {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package io.shinhanlife.dap.mcg.presentation;
|
||||
|
||||
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -30,7 +29,7 @@ public class ToolReportProxyController {
|
||||
public ToolReportProxyController(
|
||||
RestClient.Builder restClientBuilder,
|
||||
McpRouterController mcpRouterController,
|
||||
@Value("${report.service-url:http://127.0.0.1:8092}") String reportServiceUrl) {
|
||||
@Value("${report.service-url}") String reportServiceUrl) {
|
||||
this.restClient = restClientBuilder.build();
|
||||
this.mcpRouterController = mcpRouterController;
|
||||
this.reportServiceUrl = reportServiceUrl.replaceAll("/+$", "");
|
||||
@@ -75,18 +74,7 @@ public class ToolReportProxyController {
|
||||
}
|
||||
|
||||
private List<ToolMetadata> activeTools() {
|
||||
ResponseEntity<JsonRpcResponse> response = mcpRouterController.listTools(null);
|
||||
JsonRpcResponse body = response.getBody();
|
||||
if (body == null || !(body.getResult() instanceof Map<?, ?> result)
|
||||
|| !(result.get("tools") instanceof List<?> tools)) {
|
||||
throw new IllegalStateException("MCP tools/list response does not contain result.tools");
|
||||
}
|
||||
return tools.stream().map(item -> {
|
||||
if (!(item instanceof ToolMetadata metadata)) {
|
||||
throw new IllegalStateException("MCP tools/list contains an invalid tool entry");
|
||||
}
|
||||
return metadata;
|
||||
}).toList();
|
||||
return mcpRouterController.activeToolsForReport();
|
||||
}
|
||||
|
||||
private ResponseEntity<byte[]> copy(ResponseEntity<byte[]> response) {
|
||||
|
||||
@@ -52,3 +52,6 @@ mcp:
|
||||
logging:
|
||||
level:
|
||||
org.apache.kafka: ERROR
|
||||
|
||||
report:
|
||||
service-url: ${REPORT_SERVICE_URL:http://127.0.0.1:8092}
|
||||
|
||||
@@ -24,6 +24,9 @@ server:
|
||||
enabled: true
|
||||
shutdown: graceful
|
||||
|
||||
report:
|
||||
service-url: ${REPORT_SERVICE_URL:http://tool-report:8092}
|
||||
|
||||
mcp:
|
||||
gateway:
|
||||
fallback:
|
||||
|
||||
@@ -760,6 +760,7 @@
|
||||
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
|
||||
<a href="/tester.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Tester</a>
|
||||
<a href="/tool-test-console.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Console</a>
|
||||
<a href="/tool-report.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Report</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
|
||||
@@ -156,8 +156,15 @@
|
||||
async function load() {
|
||||
try {
|
||||
const response = await fetch('/report/api/report-tools');
|
||||
if (!response.ok) throw new Error('툴 목록 조회에 실패했습니다.');
|
||||
state.tools = await response.json();
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(body?.error || body?.message || `툴 목록 조회에 실패했습니다. (${response.status})`);
|
||||
}
|
||||
const tools = Array.isArray(body) ? body : body?.result?.tools;
|
||||
if (!Array.isArray(tools)) {
|
||||
throw new Error('툴 목록 응답 형식이 올바르지 않습니다.');
|
||||
}
|
||||
state.tools = tools;
|
||||
[...new Set(state.tools.map(tool => tool.categoryKey))].sort().forEach(category => {
|
||||
const option = document.createElement('option');
|
||||
option.value = category; option.textContent = category; el('category').appendChild(option);
|
||||
|
||||
@@ -5,6 +5,7 @@ plugins {
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1'
|
||||
implementation 'com.github.javaparser:javaparser-core:3.26.3'
|
||||
implementation 'org.apache.poi:poi-ooxml:5.3.0'
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ public class ToolReportExcelWriter {
|
||||
try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
Styles styles = new Styles(workbook);
|
||||
writeSummary(workbook, reports, styles);
|
||||
writeDefinitions(workbook, reports, styles);
|
||||
writeFields(workbook, reports, styles);
|
||||
writeDiagnostics(workbook, reports, styles);
|
||||
workbook.write(output);
|
||||
@@ -45,6 +46,29 @@ public class ToolReportExcelWriter {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeDefinitions(Workbook workbook, List<ToolReportModel> reports, Styles styles) {
|
||||
String[] headers = {"No.", "툴명", "버전", "표시 설명", "기능 설명", "사용 시점", "사용 제외",
|
||||
"입출력 제한", "예시 질의", "태그", "필수 환경변수", "소유 조직", "정의 파일"};
|
||||
Sheet sheet = workbook.createSheet("툴 정의");
|
||||
decorateSheet(sheet, "Tool Definition Report", "tool-definitions YAML 메타데이터", reports,
|
||||
headers.length, styles);
|
||||
header(sheet, headers, styles);
|
||||
int rowIndex = FIRST_DATA_ROW;
|
||||
int sequence = 1;
|
||||
for (ToolReportModel report : reports) {
|
||||
ToolSummary tool = report.tool();
|
||||
Row row = sheet.createRow(rowIndex++);
|
||||
Object[] values = {sequence++, tool.name(), tool.version(), tool.displayDescription(),
|
||||
tool.functionDescription(), tool.whenToUse(), tool.whenNotToUse(), tool.ioLimits(),
|
||||
tool.exampleQueries(), tool.tags(), tool.requiredEnvKeys(), tool.ownerOrg(), tool.definitionFile()};
|
||||
values(row, values, styles.body);
|
||||
row.getCell(0).setCellStyle(styles.bodyCenter);
|
||||
row.getCell(2).setCellStyle(styles.bodyCenter);
|
||||
}
|
||||
finishTable(sheet, rowIndex, headers.length,
|
||||
new int[]{7, 30, 12, 40, 48, 48, 48, 48, 60, 30, 30, 20, 65});
|
||||
}
|
||||
|
||||
private void writeSummary(Workbook workbook, List<ToolReportModel> reports, Styles styles) {
|
||||
String[] headers = {"No.", "툴명", "제목", "설명", "카테고리", "연계 ID", "등록", "승인 필요",
|
||||
"Read Only", "Destructive", "Idempotent", "Open World", "Request", "Response", "UseCase", "원천 파일"};
|
||||
|
||||
@@ -18,5 +18,25 @@ public record ToolSummary(
|
||||
String useCaseClass,
|
||||
String sourceFile,
|
||||
String inputSchemaResource,
|
||||
String outputSchemaResource) {
|
||||
String outputSchemaResource,
|
||||
String version,
|
||||
String functionDescription,
|
||||
String whenToUse,
|
||||
String whenNotToUse,
|
||||
String ioLimits,
|
||||
String displayDescription,
|
||||
String exampleQueries,
|
||||
String tags,
|
||||
String requiredEnvKeys,
|
||||
String ownerOrg,
|
||||
String definitionFile) {
|
||||
|
||||
public ToolSummary(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) {
|
||||
this(name, title, description, categoryKey, mappingId, register, requiresApproval, readOnlyHint,
|
||||
destructiveHint, idempotentHint, openWorldHint, requestType, responseType, useCaseClass, sourceFile,
|
||||
inputSchemaResource, outputSchemaResource, "", "", "", "", "", "", "", "", "", "", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.shinhanlife.dap.report.source;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.FieldDeclaration;
|
||||
@@ -26,10 +27,12 @@ public class ToolDetailAnalyzer {
|
||||
|
||||
private final ToolSourceDiscovery discovery;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ObjectMapper yamlMapper;
|
||||
|
||||
public ToolDetailAnalyzer(ToolSourceDiscovery discovery) {
|
||||
this.discovery = discovery;
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||
}
|
||||
|
||||
public ToolReportModel analyze(ToolSummary tool) {
|
||||
@@ -39,10 +42,12 @@ public class ToolDetailAnalyzer {
|
||||
|
||||
collectSchema(tool, tool.inputSchemaResource(), "INPUT", fields, diagnostics);
|
||||
collectSchema(tool, tool.outputSchemaResource(), "OUTPUT", fields, diagnostics);
|
||||
if (tool.inputSchemaResource().isBlank()) {
|
||||
boolean definitionInput = collectDefinitionSchema(tool, "parameters_schema", "INPUT", fields, diagnostics);
|
||||
boolean definitionOutput = collectDefinitionSchema(tool, "output_schema", "OUTPUT", fields, diagnostics);
|
||||
if (tool.inputSchemaResource().isBlank() && !definitionInput) {
|
||||
collectJavaType(tool, javaFiles.get(tool.requestType()), "DTO", "INPUT", fields, diagnostics);
|
||||
}
|
||||
if (tool.outputSchemaResource().isBlank()) {
|
||||
if (tool.outputSchemaResource().isBlank() && !definitionOutput) {
|
||||
collectJavaType(tool, javaFiles.get(tool.responseType()), "DTO", "OUTPUT", fields, diagnostics);
|
||||
}
|
||||
if (!tool.mappingId().isBlank()) {
|
||||
@@ -52,6 +57,21 @@ public class ToolDetailAnalyzer {
|
||||
return new ToolReportModel(tool, List.copyOf(fields), List.copyOf(diagnostics));
|
||||
}
|
||||
|
||||
private boolean collectDefinitionSchema(ToolSummary tool, String schemaKey, String direction,
|
||||
List<FieldDefinition> fields, List<String> diagnostics) {
|
||||
if (tool.definitionFile() == null || tool.definitionFile().isBlank()) return false;
|
||||
Path path = discovery.sourceRoot().resolve(tool.definitionFile()).normalize();
|
||||
try {
|
||||
JsonNode schema = yamlMapper.readTree(path.toFile()).path(schemaKey);
|
||||
if (schema.isMissingNode() || schema.isNull() || schema.isEmpty()) return false;
|
||||
collectSchemaFields(tool, schema, "TOOL_DEFINITION", direction, "", "", relative(path), fields);
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
diagnostics.add("Tool definition schema 분석 실패: " + relative(path) + " (" + schemaKey + ")");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Path> indexJavaFiles() {
|
||||
Map<String, Path> result = new LinkedHashMap<>();
|
||||
try (var paths = Files.walk(discovery.sourceRoot())) {
|
||||
@@ -83,23 +103,41 @@ public class ToolDetailAnalyzer {
|
||||
Path path = matches.get(0);
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(path.toFile());
|
||||
JsonNode properties = root.path("properties");
|
||||
List<String> required = new ArrayList<>();
|
||||
root.path("required").forEach(node -> required.add(node.asText()));
|
||||
Iterator<Map.Entry<String, JsonNode>> iterator = properties.fields();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, JsonNode> entry = iterator.next();
|
||||
JsonNode definition = entry.getValue();
|
||||
fields.add(new FieldDefinition(tool.name(), "JSON_SCHEMA", direction, "",
|
||||
entry.getKey(), definition.path("type").asText("object"),
|
||||
required.contains(entry.getKey()), definition.path("description").asText(""),
|
||||
constraints(definition), relative(path)));
|
||||
}
|
||||
collectSchemaFields(tool, root, "JSON_SCHEMA", direction, "", "", relative(path), fields);
|
||||
} catch (Exception exception) {
|
||||
diagnostics.add("Schema 분석 실패: " + relative(path));
|
||||
}
|
||||
}
|
||||
|
||||
private void collectSchemaFields(ToolSummary tool, JsonNode schema, String sourceKind, String direction,
|
||||
String ownerPath, String fieldPrefix, String sourceFile,
|
||||
List<FieldDefinition> fields) {
|
||||
List<String> required = new ArrayList<>();
|
||||
schema.path("required").forEach(node -> required.add(node.asText()));
|
||||
Iterator<Map.Entry<String, JsonNode>> iterator = schema.path("properties").fields();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, JsonNode> entry = iterator.next();
|
||||
JsonNode definition = entry.getValue();
|
||||
String fieldName = fieldPrefix.isBlank() ? entry.getKey() : fieldPrefix + "." + entry.getKey();
|
||||
String type = schemaType(definition);
|
||||
fields.add(new FieldDefinition(tool.name(), sourceKind, direction, ownerPath, fieldName, type,
|
||||
required.contains(entry.getKey()), definition.path("description").asText(""),
|
||||
constraints(definition), sourceFile));
|
||||
JsonNode nested = definition.path("properties").isObject() ? definition : definition.path("items");
|
||||
if (nested.path("properties").isObject()) {
|
||||
collectSchemaFields(tool, nested, sourceKind, direction, fieldName, fieldName, sourceFile, fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String schemaType(JsonNode definition) {
|
||||
String type = definition.path("type").asText("object");
|
||||
if ("array".equals(type)) {
|
||||
return "array<" + definition.path("items").path("type").asText("object") + ">";
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private void collectJavaType(ToolSummary tool, Path path, String sourceKind, String direction,
|
||||
List<FieldDefinition> fields, List<String> diagnostics) {
|
||||
if (path == null) {
|
||||
|
||||
@@ -4,6 +4,9 @@ import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.MethodDeclaration;
|
||||
import com.github.javaparser.ast.expr.AnnotationExpr;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import io.shinhanlife.dap.report.config.ReportProperties;
|
||||
import io.shinhanlife.dap.report.model.ToolSummary;
|
||||
import java.io.IOException;
|
||||
@@ -13,6 +16,7 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -22,6 +26,7 @@ import org.springframework.stereotype.Component;
|
||||
public class ToolSourceDiscovery {
|
||||
|
||||
private final Path sourceRoot;
|
||||
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||
|
||||
public ToolSourceDiscovery(ReportProperties properties) {
|
||||
String configuredRoot = properties.sourceRoot();
|
||||
@@ -45,15 +50,89 @@ public class ToolSourceDiscovery {
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Failed to scan tool sources: " + sourceRoot, exception);
|
||||
}
|
||||
return tools.stream()
|
||||
Map<String, ToolSummary> discovered = tools.stream()
|
||||
.sorted(Comparator.comparing(ToolSummary::sourceFile))
|
||||
.collect(Collectors.toMap(ToolSummary::name, Function.identity(),
|
||||
(first, duplicate) -> first, LinkedHashMap::new))
|
||||
.values().stream()
|
||||
(first, duplicate) -> first, LinkedHashMap::new));
|
||||
discoverDefinitions().forEach((name, definition) ->
|
||||
discovered.merge(name, definition, this::mergeDefinition));
|
||||
return discovered.values().stream()
|
||||
.sorted(Comparator.comparing(ToolSummary::name))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Map<String, ToolSummary> discoverDefinitions() {
|
||||
Map<String, ToolSummary> definitions = new LinkedHashMap<>();
|
||||
try (var paths = Files.walk(sourceRoot)) {
|
||||
paths.filter(this::isToolDefinition).sorted().forEach(path -> {
|
||||
try {
|
||||
JsonNode root = yamlMapper.readTree(path.toFile());
|
||||
String name = text(root, "name");
|
||||
if (name.isBlank()) return;
|
||||
JsonNode description = root.path("description");
|
||||
String function = text(description, "function");
|
||||
definitions.putIfAbsent(name, new ToolSummary(
|
||||
name, text(root, "display_name"), function, text(root, "category_key"),
|
||||
text(root, "legacy_interface_id"), true, false,
|
||||
bool(root, "read_only"), bool(root, "destructive"), bool(root, "idempotent"), false,
|
||||
"", "", "", "", "", "", text(root, "version"), function,
|
||||
text(description, "when_to_use"), text(description, "when_not_to_use"),
|
||||
text(description, "io_limits"), text(root, "display_description"),
|
||||
joined(root.path("example_queries")), joined(root.path("tags")),
|
||||
joined(root.path("required_env_keys")), text(root, "owner_org"), relative(path)));
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Failed to parse tool definition: " + path, exception);
|
||||
}
|
||||
});
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Failed to scan tool definitions: " + sourceRoot, exception);
|
||||
}
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private ToolSummary mergeDefinition(ToolSummary source, ToolSummary definition) {
|
||||
return new ToolSummary(source.name(), prefer(definition.title(), source.title()),
|
||||
prefer(definition.description(), source.description()),
|
||||
prefer(definition.categoryKey(), source.categoryKey()),
|
||||
prefer(definition.mappingId(), source.mappingId()), source.register(), source.requiresApproval(),
|
||||
definition.readOnlyHint(), definition.destructiveHint(), definition.idempotentHint(),
|
||||
source.openWorldHint(), source.requestType(), source.responseType(), source.useCaseClass(),
|
||||
source.sourceFile(), source.inputSchemaResource(), source.outputSchemaResource(), definition.version(),
|
||||
definition.functionDescription(), definition.whenToUse(), definition.whenNotToUse(),
|
||||
definition.ioLimits(), definition.displayDescription(), definition.exampleQueries(), definition.tags(),
|
||||
definition.requiredEnvKeys(), definition.ownerOrg(), definition.definitionFile());
|
||||
}
|
||||
|
||||
private boolean isToolDefinition(Path path) {
|
||||
String normalized = path.toString().replace('\\', '/');
|
||||
String fileName = path.getFileName().toString().toLowerCase();
|
||||
return Files.isRegularFile(path) && (fileName.endsWith(".yml") || fileName.endsWith(".yaml"))
|
||||
&& normalized.contains("/src/main/resources/tool-definitions/");
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
return node.path(field).asText("");
|
||||
}
|
||||
|
||||
private boolean bool(JsonNode node, String field) {
|
||||
return node.path(field).asBoolean(false);
|
||||
}
|
||||
|
||||
private String joined(JsonNode node) {
|
||||
if (!node.isArray()) return node.asText("");
|
||||
List<String> values = new ArrayList<>();
|
||||
node.forEach(value -> values.add(value.asText()));
|
||||
return String.join(" | ", values);
|
||||
}
|
||||
|
||||
private String prefer(String primary, String fallback) {
|
||||
return primary == null || primary.isBlank() ? fallback : primary;
|
||||
}
|
||||
|
||||
private String relative(Path path) {
|
||||
return sourceRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/');
|
||||
}
|
||||
|
||||
private Path resolveProjectRoot(Path configuredPath) {
|
||||
Path candidate = configuredPath;
|
||||
while (candidate != null) {
|
||||
@@ -88,7 +167,8 @@ public class ToolSourceDiscovery {
|
||||
}
|
||||
|
||||
private ToolSummary toSummary(Path path, String owner, MethodDeclaration method, AnnotationExpr tool) {
|
||||
AnnotationExpr hint = JavaAnnotationReader.find(method.getAnnotations(), "ToolHint").orElse(null);
|
||||
AnnotationExpr hint = JavaAnnotationReader.find(method.getAnnotations(), "GrowToolHint")
|
||||
.or(() -> JavaAnnotationReader.find(method.getAnnotations(), "ToolHint")).orElse(null);
|
||||
AnnotationExpr annotations = JavaAnnotationReader.nested(tool, "annotations").orElse(null);
|
||||
String name = JavaAnnotationReader.string(tool, "name", method.getNameAsString());
|
||||
String title = JavaAnnotationReader.string(tool, "title", name);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
server:
|
||||
address: ${REPORT_SERVER_ADDRESS:127.0.0.1}
|
||||
address: ${REPORT_SERVER_ADDRESS:0.0.0.0}
|
||||
port: ${REPORT_SERVER_PORT:8092}
|
||||
|
||||
spring:
|
||||
|
||||
@@ -21,8 +21,9 @@ class ToolReportExcelWriterTest {
|
||||
byte[] content = new ToolReportExcelWriter().write(List.of(new ToolReportModel(tool, List.of(field), List.of())));
|
||||
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
|
||||
assertThat(workbook.getNumberOfSheets()).isEqualTo(3);
|
||||
assertThat(workbook.getNumberOfSheets()).isEqualTo(4);
|
||||
assertThat(workbook.getSheet("툴 기본정보").getRow(8).getCell(1).getStringCellValue()).isEqualTo(tool.name());
|
||||
assertThat(workbook.getSheet("툴 정의").getRow(8).getCell(1).getStringCellValue()).isEqualTo(tool.name());
|
||||
assertThat(workbook.getSheet("수집 필드").getRow(8).getCell(5).getStringCellValue()).isEqualTo("customerNo");
|
||||
assertThat(workbook.getSheet("툴 기본정보").getRow(0).getCell(0).getCellStyle().getFillForegroundColorColor().getARGBHex())
|
||||
.endsWith("262626");
|
||||
|
||||
@@ -16,6 +16,36 @@ class ToolSourceDiscoveryTest {
|
||||
|
||||
assertThat(discovery.discover())
|
||||
.extracting(tool -> tool.name())
|
||||
.contains("oth.cmm.customer.detail", "sms.sms.msg.send");
|
||||
.contains("cmm_customer_tool", "cmm_claim_search", "iam_system_status");
|
||||
|
||||
assertThat(discovery.discover().stream()
|
||||
.filter(tool -> tool.name().equals("cmm_customer_tool"))
|
||||
.findFirst().orElseThrow())
|
||||
.satisfies(tool -> {
|
||||
assertThat(tool.version()).isEqualTo("1.0.0");
|
||||
assertThat(tool.mappingId()).isEqualTo("ONILD0320");
|
||||
assertThat(tool.definitionFile()).endsWith("tool-definitions/cmm/cmm_customer_tool.yml");
|
||||
assertThat(tool.whenToUse()).contains("고객 ID");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void collectsJsonSchemaFieldsEmbeddedInToolDefinitionYaml() {
|
||||
Path projectRoot = Path.of("..").toAbsolutePath().normalize();
|
||||
ToolSourceDiscovery discovery = new ToolSourceDiscovery(
|
||||
new ReportProperties(projectRoot.toString(), "tool-report"));
|
||||
var tool = discovery.discover().stream()
|
||||
.filter(candidate -> candidate.name().equals("cmm_customer_tool"))
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
var report = new ToolDetailAnalyzer(discovery).analyze(tool);
|
||||
|
||||
assertThat(report.fields())
|
||||
.anySatisfy(field -> {
|
||||
assertThat(field.sourceKind()).isEqualTo("TOOL_DEFINITION");
|
||||
assertThat(field.direction()).isEqualTo("INPUT");
|
||||
assertThat(field.fieldName()).isEqualTo("csNo");
|
||||
assertThat(field.required()).isTrue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<a href="/chat.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Chat</a>
|
||||
<a href="/tester.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Tester</a>
|
||||
<a href="/tool-test-console.html" style="color:#ffffff; text-decoration:none;" class="font-semibold">Console</a>
|
||||
<a href="/tool-report.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Report</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
Reference in New Issue
Block a user