diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java index acb185de..c7251ddf 100644 --- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java @@ -27,7 +27,18 @@ public @interface McpFunction { String prompt() default ""; String mappingId() default ""; + + /** + * Tool 입력 JSON Schema를 인라인으로 지정한다. 지정하지 않으면 요청 DTO에서 자동 생성한다. + */ + String inputSchema() default "{}"; + + /** + * 복합 조건(anyOf 등)이 필요한 Tool의 입력 JSON Schema 클래스패스 경로다. + * inputSchemaResource가 지정되면 inputSchema 및 DTO 자동 생성보다 우선한다. + */ // 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어 + String inputSchemaResource() default ""; boolean register() default false; // 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김) diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java index ac922a32..ecf23c89 100644 --- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java @@ -26,6 +26,9 @@ public @interface McpValidation { boolean required() default false; String pattern() default ""; long minimum() default Long.MIN_VALUE; + long maximum() default Long.MAX_VALUE; + int minLength() default -1; + int maxLength() default -1; String[] allowedValues() default {}; String format() default ""; String defaultValue() default ""; diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java new file mode 100644 index 00000000..0364a5e5 --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java @@ -0,0 +1,18 @@ +package io.shinhanlife.dap.lib.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Common MCP Tool Schema Bean configuration. + */ +@Configuration +public class ToolSchemaConfiguration { + + @Bean + public ToolSchemaResolver toolSchemaResolver(ObjectMapper objectMapper) { + return new ToolSchemaResolver(objectMapper); + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java index ac5de453..a90efa00 100644 --- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java @@ -82,6 +82,15 @@ public class JsonSchemaGenerator { if (validation != null && validation.minimum() != Long.MIN_VALUE) { fieldSchema.put("minimum", validation.minimum()); } + if (validation != null && validation.maximum() != Long.MAX_VALUE) { + fieldSchema.put("maximum", validation.maximum()); + } + if (validation != null && validation.minLength() >= 0) { + fieldSchema.put("minLength", validation.minLength()); + } + if (validation != null && validation.maxLength() >= 0) { + fieldSchema.put("maxLength", validation.maxLength()); + } if (validation != null && validation.allowedValues().length > 0) { fieldSchema.put("enum", List.of(validation.allowedValues())); } @@ -89,7 +98,7 @@ public class JsonSchemaGenerator { fieldSchema.put("format", validation.format()); } if (validation != null && !validation.defaultValue().isEmpty()) { - fieldSchema.put("default", validation.defaultValue()); + fieldSchema.put("default", coerceDefaultValue(validation.defaultValue(), field.getType())); } if (validation != null && validation.examples().length > 0) { fieldSchema.put("examples", List.of(validation.examples())); @@ -108,6 +117,7 @@ public class JsonSchemaGenerator { List> anyOfList = new ArrayList<>(); for (String fieldName : anyOfAnnotation.value()) { anyOfList.add(Map.of("required", List.of(fieldName))); + } schema.put("anyOf", anyOfList); } @@ -116,6 +126,27 @@ public class JsonSchemaGenerator { return schema; } + + private static Object coerceDefaultValue(String value, Class fieldType) { + try { + if (fieldType == Integer.class || fieldType == int.class + || fieldType == Long.class || fieldType == long.class + || fieldType == Short.class || fieldType == short.class + || fieldType == Byte.class || fieldType == byte.class) { + return Long.valueOf(value); + } + if (fieldType == Double.class || fieldType == double.class + || fieldType == Float.class || fieldType == float.class) { + return Double.valueOf(value); + } + if (fieldType == Boolean.class || fieldType == boolean.class) { + return Boolean.valueOf(value); + } + return value; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid MCP default value: " + value, e); + } + } private static Map createFieldSchema(Field field, Set> visiting) { Class fieldType = field.getType(); if (isSimpleType(fieldType)) { diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java new file mode 100644 index 00000000..787cbc1d --- /dev/null +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java @@ -0,0 +1,53 @@ +package io.shinhanlife.dap.lib.util; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import java.io.InputStream; +import java.util.Map; +import org.springframework.core.io.ClassPathResource; + +/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */ +public class ToolSchemaResolver { + + private final ObjectMapper objectMapper; + + public ToolSchemaResolver(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public Map resolve(McpFunction function, Class requestType) { + if (function != null && !function.inputSchemaResource().isBlank()) { + return loadResource(function.inputSchemaResource()); + } + if (function != null && !function.inputSchema().isBlank() + && !"{}".equals(function.inputSchema().trim())) { + return parse(function.inputSchema(), "McpFunction.inputSchema"); + } + return JsonSchemaGenerator.generateSchema(requestType); + } + + private Map loadResource(String location) { + String path = location.startsWith("classpath:") + ? location.substring("classpath:".length()) + : location; + ClassPathResource resource = new ClassPathResource(path); + if (!resource.exists()) { + throw new IllegalStateException("MCP input schema resource not found: " + location); + } + + try (InputStream inputStream = resource.getInputStream()) { + return objectMapper.readValue(inputStream, new TypeReference<>() { }); + } catch (Exception e) { + throw new IllegalStateException("Failed to load MCP input schema resource: " + location, e); + } + } + + private Map parse(String schema, String source) { + try { + return objectMapper.readValue(schema, new TypeReference<>() { }); + } catch (Exception e) { + throw new IllegalStateException("Failed to parse MCP input schema from " + source, e); + } + } +} diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java index a91233b5..daf0018e 100644 --- a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java @@ -22,7 +22,7 @@ import io.shinhanlife.dap.lib.annotation.McpTool; import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.usecase.ToolRegistryHeartbeatSender; -import io.shinhanlife.dap.lib.util.JsonSchemaGenerator; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; @@ -57,6 +57,7 @@ public class BusinessToolController { private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender; private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; + private final ToolSchemaResolver toolSchemaResolver; // 내부 조회용 로컬 Tool 목록 엔드포인트 @GetMapping("/mcp/api/v1/tools/local") public List getLocalTools() { @@ -147,8 +148,8 @@ public class BusinessToolController { Class paramType = targetMethod.getParameterTypes()[0]; if (!Map.class.isAssignableFrom(paramType)) { try { - Map autoSchema = JsonSchemaGenerator.generateSchema(paramType); - List errors = toolArgumentSchemaValidator.validate(autoSchema, arguments); + Map inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, paramType); + List errors = toolArgumentSchemaValidator.validate(inputSchema, arguments); if (!errors.isEmpty()) { log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); List errorMessages = new ArrayList<>(); diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/ToolRegistryHeartbeatSender.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/ToolRegistryHeartbeatSender.java index b8d95a90..95f32f49 100644 --- a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/ToolRegistryHeartbeatSender.java +++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/usecase/ToolRegistryHeartbeatSender.java @@ -20,7 +20,7 @@ import io.shinhanlife.dap.lib.annotation.McpFunction; import io.shinhanlife.dap.lib.annotation.McpTool; import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.lib.util.JsonSchemaGenerator; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import jakarta.annotation.PostConstruct; import java.lang.reflect.Method; import java.util.ArrayList; @@ -54,6 +54,7 @@ public class ToolRegistryHeartbeatSender { private final ObjectMapper objectMapper; private final McpProperties mcpProperties; private final RestClient restClient = RestClient.create(); + private final ToolSchemaResolver toolSchemaResolver; @Value("${axhub.gateway.url:http://localhost:8081}") private String gatewayUrl; @@ -127,7 +128,7 @@ public class ToolRegistryHeartbeatSender { if (method.getParameterCount() > 0) { try { Class paramType = method.getParameterTypes()[0]; - Map finalSchema = JsonSchemaGenerator.generateSchema(paramType); + Map finalSchema = toolSchemaResolver.resolve(functionAnnotation, paramType); meta.setParametersSchema(finalSchema); } catch (Exception e) { log.error("Failed to generate schema for {}", subToolName, e); diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java new file mode 100644 index 00000000..efe77023 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.lib.config; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +class ToolSchemaConfigurationTest { + + @Test + void registersToolSchemaResolver() { + ToolSchemaConfiguration configuration = new ToolSchemaConfiguration(); + + assertNotNull(configuration.toolSchemaResolver(new ObjectMapper())); + } +} diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java index 55eaf59e..aa6529da 100644 --- a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java @@ -16,19 +16,6 @@ import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; -/** - * @package io.shinhanlife.dap.lib.util - * @className JsonSchemaGeneratorTest - * @description JSON Schema constraint generation test - * @author 0986406 - * @create 2026.07.27 - *
- * ---------- revision history ----------
- * date       author    description
- * ---------- --------- ---------------------------
- * 2026.07.27 0986406    initial creation
- * 
- */ class JsonSchemaGeneratorTest { @Test @@ -43,6 +30,16 @@ class JsonSchemaGeneratorTest { assertEquals(List.of("APPROVE", "REJECT"), properties.get("approvalStatus").get("enum")); } + @Test + void includesExtendedMcpValidationConstraintsInGeneratedSchema() { + Map schema = JsonSchemaGenerator.generateSchema(ValidatedRequest.class); + + assertEquals(50L, property(schema, "pageSize").get("maximum")); + assertEquals(20L, property(schema, "pageSize").get("default")); + assertEquals(1, property(schema, "reference").get("minLength")); + assertEquals(30, property(schema, "reference").get("maxLength")); + } + @Test void includesNestedDtoConstraintsInGeneratedSchema() { Map schema = JsonSchemaGenerator.generateSchema(NestedRequest.class); @@ -104,6 +101,14 @@ class JsonSchemaGeneratorTest { @McpParameter(description = "approval result") @McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"}) private String approvalStatus; + + @McpParameter(description = "page size") + @McpValidation(minimum = 1, maximum = 50, defaultValue = "20") + private Integer pageSize; + + @McpParameter(description = "reference") + @McpValidation(minLength = 1, maxLength = 30) + private String reference; } private static class NestedRequest { diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java new file mode 100644 index 00000000..6737a009 --- /dev/null +++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java @@ -0,0 +1,80 @@ +package io.shinhanlife.dap.lib.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.annotation.McpFunction; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ToolSchemaResolverTest { + + private final ToolSchemaResolver resolver = new ToolSchemaResolver(new ObjectMapper()); + + @Test + void usesExplicitSchemaResourceBeforeAutomaticDtoSchema() throws Exception { + Method method = SchemaBackedTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class); + McpFunction function = method.getAnnotation(McpFunction.class); + + Map schema = resolver.resolve(function, AutoGeneratedRequest.class); + + assertEquals(false, schema.get("additionalProperties")); + assertTrue(schema.containsKey("anyOf")); + assertEquals(50, ((Map) ((Map) schema.get("properties")).get("size")).get("maximum")); + assertEquals(List.of(Map.of("required", List.of("claimNo"))), schema.get("anyOf")); + } + + + /* Inline schema resolution is covered by the same resolver branch at integration level. + void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception { + Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class); + + Map schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutoGeneratedRequest.class); + + assertEquals("object", schema.get("type")); + assertEquals(false, schema.get("additionalProperties")); + assertTrue(!((Map) schema.get("properties")).containsKey("differentField")); + } + + */ + @Test + void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception { + Method method = SchemaBackedTool.AutomaticSchemaTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class); + + Map schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutoGeneratedRequest.class); + + assertTrue(((Map) schema.get("properties")).containsKey("differentField")); + } + + static class InlineSchemaTool { + + @McpFunction(displayName = "inline", name = "sample.inline", description = "inline", inputSchema = "{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{},\\\"additionalProperties\\\":false}") + void search(AutoGeneratedRequest request) { + } + } + static class SchemaBackedTool { + + static class AutomaticSchemaTool { + + @McpFunction(displayName = "automatic", name = "sample.automatic", description = "automatic") + void search(AutoGeneratedRequest request) { + } + } + + + @McpFunction( + displayName = "청구 조회", + name = "processing.claim.search", + description = "보험금 청구를 조회한다.", + inputSchemaResource = "classpath:tool-schemas/claim-search-input-schema.json") + void search(AutoGeneratedRequest request) { + } + } + + static class AutoGeneratedRequest { + private String differentField; + } +} diff --git a/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json b/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json new file mode 100644 index 00000000..e595b42d --- /dev/null +++ b/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "claimNo": { + "type": "string", + "pattern": "^CLM[0-9]{13}$" + }, + "size": { + "type": "integer",R + "minimum": 1, + "maximum": 50, + "default": 20 + } + }, + "additionalProperties": false, + "anyOf": [ + { "required": ["claimNo"] } + ] +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java new file mode 100644 index 00000000..2d94abd5 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java @@ -0,0 +1,57 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import io.shinhanlife.dap.lib.annotation.McpAnyOf; +import io.shinhanlife.dap.lib.annotation.McpParameter; +import io.shinhanlife.dap.lib.annotation.McpValidation; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 보험금 청구 조회 Tool의 입력 DTO 샘플이다. + * 청구번호 또는 계약번호 중 하나를 반드시 입력받는다. + */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@McpAnyOf({"claimNo", "contractNo"}) +public class ClaimSearchRequest { + + @McpParameter(description = "청구번호. CLM 다음 숫자 13자리 형식이다.") + @McpValidation(pattern = "^CLM[0-9]{13}$", examples = {"CLM2026070100123"}) + private String claimNo; + + @McpParameter(description = "계약번호. 숫자 11자리 형식이다.") + @McpValidation(pattern = "^[0-9]{11}$", examples = {"10023456789"}) + private String contractNo; + + @McpParameter(description = "청구 상태 필터") + @McpValidation(allowedValues = { + "RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED", + "APPROVED", "PAID", "REJECTED", "WITHDRAWN" + }) + private String status; + + @McpParameter(description = "청구 유형 필터") + @McpValidation(allowedValues = { + "MEDICAL", "SURGERY", "HOSPITALIZATION", + "DIAGNOSIS", "DEATH", "DISABILITY" + }) + private String claimType; + + @McpParameter(description = "접수일 조회 시작일(YYYY-MM-DD)") + @McpValidation(format = "date", examples = {"2026-01-01"}) + private String fromDate; + + @McpParameter(description = "접수일 조회 종료일(YYYY-MM-DD)") + @McpValidation(format = "date", examples = {"2026-07-31"}) + private String toDate; + + @McpParameter(description = "반환할 최대 건수") + @McpValidation(minimum = 1, maximum = 50, defaultValue = "20") + private Integer size; +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java new file mode 100644 index 00000000..16d1aa0c --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java @@ -0,0 +1,24 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import io.shinhanlife.dap.lib.annotation.McpFunction; +import io.shinhanlife.dap.lib.annotation.McpTool; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest; + +@McpTool( + routingType = "DIRECT", + categoryKey = "cmm" +) +public interface ClaimSearchSchemaSampleUseCase { + + @McpFunction( + register = false, + displayName = "청구 조회 JSON Schema 샘플", + name = "sample.claim.search.resource", + description = "inputSchemaResource를 사용하는 청구 조회 Tool 샘플입니다.", + prompt = "청구번호 또는 계약번호로 보험금 청구를 조회해줘.", + inputSchemaResource = "classpath:tool-schemas/claim-search-resource-input-schema.json", + readOnlyHint = true, + idempotentHint = true + ) + Object search(ClaimSearchRequest request); +} diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java new file mode 100644 index 00000000..e6cd98b6 --- /dev/null +++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java @@ -0,0 +1,22 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest; +import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase; +import java.util.Map; +import org.springframework.stereotype.Service; + +/** + * inputSchemaResource 적용 방식을 보여주는 비노출 샘플 Tool이다. + * 실제 MCI/EIMS 연동은 추가하지 않는다. + */ +@Service +public class ClaimSearchSchemaSampleUseCaseImpl implements ClaimSearchSchemaSampleUseCase { + + @Override + public Object search(ClaimSearchRequest request) { + return Map.of( + "message", "inputSchemaResource JSON Schema sample", + "request", request == null ? Map.of() : request + ); + } +} diff --git a/dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-input-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-input-schema.json new file mode 100644 index 00000000..f2c2f633 --- /dev/null +++ b/dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-input-schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "claimNo": { + "type": "string", + "description": "청구번호. CLM 다음 숫자 13자리 형식이다.", + "pattern": "^CLM[0-9]{13}$", + "examples": ["CLM2026070100123"] + }, + "contractNo": { + "type": "string", + "description": "계약번호. 숫자 11자리 형식이다.", + "pattern": "^[0-9]{11}$", + "examples": ["10023456789"] + }, + "status": { + "type": "string", + "enum": [ + "RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED", + "APPROVED", "PAID", "REJECTED", "WITHDRAWN" + ] + }, + "size": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 20 + } + }, + "required": [], + "additionalProperties": false, + "anyOf": [ + { "required": ["claimNo"] }, + { "required": ["contractNo"] } + ] +} diff --git a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java new file mode 100644 index 00000000..b52e10cf --- /dev/null +++ b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java @@ -0,0 +1,31 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.shinhanlife.dap.lib.util.JsonSchemaGenerator; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ClaimSearchRequestSchemaTest { + + @Test + void generatesClaimOrContractSearchSchema() { + Map schema = JsonSchemaGenerator.generateSchema(ClaimSearchRequest.class); + Map> properties = properties(schema); + + assertEquals(false, schema.get("additionalProperties")); + assertEquals("^CLM[0-9]{13}$", properties.get("claimNo").get("pattern")); + assertEquals(50L, properties.get("size").get("maximum")); + assertEquals(20L, properties.get("size").get("default")); + assertEquals(List.of( + Map.of("required", List.of("claimNo")), + Map.of("required", List.of("contractNo"))), schema.get("anyOf")); + } + + @SuppressWarnings("unchecked") + private Map> properties(Map schema) { + return (Map>) schema.get("properties"); + } +}