Compare commits

..

40 Commits

Author SHA1 Message Date
jade
7dca243380 fix: add io.shinhanlife.dap.common.integration to component scan paths across all application modules 2026-07-21 17:09:46 +09:00
jade
c90654bd1b refactor: move AxhubMciComponent and GlowMockConfig to core module for common usage 2026-07-21 17:07:02 +09:00
jade
334af3e40e fix: register GlowMciComponent as Spring Bean via config to resolve IntelliJ autowiring error 2026-07-21 17:02:57 +09:00
jade
ea27ed621a fix: resolve IntelliJ autowiring and inspection warnings for AxhubMciComponent 2026-07-21 16:59:52 +09:00
jade
fbe7091975 refactor: rename LicoMciComponent to AxhubMciComponent 2026-07-21 16:57:10 +09:00
jade
7ccca1812f refactor: apply GlowMciComponent architecture as requested by Shinhan Life standard 2026-07-21 16:53:31 +09:00
jade
123fdb30a9 fix: correct Onnba3011ReqDto fields from new API spec and fix OnnbaMciToolService compile error 2026-07-21 16:42:43 +09:00
jade
0ed927f845 feat: update Onnba3011ReqDto fields from API spec 2026-07-21 16:38:54 +09:00
jade
60ac3bc4be refactor: extract Onnba3011ReqDto into separate file 2026-07-21 16:32:40 +09:00
jade
0afa489165 feat: add OnnbaMciToolService converted from legacy MCI code 2026-07-21 15:56:37 +09:00
jade
8cd07a255c fix: add sample category to pre-initialized servers to avoid 400 bad request 2026-07-21 15:13:31 +09:00
jade
cc98d173ad fix: set register=true for sample tools to enable auto-registration on Render 2026-07-21 15:04:25 +09:00
jade
c450f26d53 fix: correct gateway URL in application-dev.properties for all tools 2026-07-21 14:55:06 +09:00
jade
7660db3c38 fix: disable auto namespace prefix in PodScaffolder 2026-07-21 14:24:49 +09:00
jade
beed6e7001 docs: add sample category to scaffolder and readme 2026-07-21 14:22:22 +09:00
jade
8612dd6ea7 feat: rename sample tools 2026-07-21 14:19:08 +09:00
jade
db6ed5b09a feat: add get_exchange_rate and get_daily_quote tools and set categoryKey to sample 2026-07-21 14:17:36 +09:00
jade
db949be3f3 feat: generate and log new request-id for outbound HTTP calls in WeatherTool 2026-07-21 14:12:58 +09:00
jade
9edcdab450 관련 소스 수정 2026-07-21 14:05:39 +09:00
jade
82b1e6e267 feat: rename tool get_weather to weather 2026-07-21 14:04:48 +09:00
jade
e35b378d61 fix: wrap execute tool response to match expected client format 2026-07-21 14:00:51 +09:00
jade
f8d6ed8da3 feat: remove namespace prefix from all remaining tool modules 2026-07-21 13:52:37 +09:00
jade
437d061bef feat: remove namespace prefix from dap-tool-other 2026-07-21 13:50:54 +09:00
jade
9cce77a142 fix: add missing register=false to register_vacation tool 2026-07-21 13:45:03 +09:00
jade
115aafa220 fix: swap trace-id and request-id header mapping 2026-07-21 13:43:13 +09:00
jade
da7ed2e28b fix: remove duplicate register property in TemplateUtilityService 2026-07-21 13:32:40 +09:00
jade
48ac7b9f9f chore: disable redis heartbeat registration for all tools (register=false) 2026-07-21 13:26:02 +09:00
jade
580edd98a6 feat: add trace-id and request-id header logging to tool controller 2026-07-21 13:18:38 +09:00
jade
b9966bbbda feat: rename weather tool from get_current_weather to get_weather 2026-07-21 13:10:52 +09:00
jade
438e30f987 refactor: change internal gateway-to-tool routing from JSON-RPC to REST API 2026-07-21 12:50:28 +09:00
jade
fe9e00b5fb feat: add missing globalId to SlCommonHeaderDto 2026-07-21 10:48:13 +09:00
jade
b0f12b578d feat: add SL and OL standard header DTOs from spec 2026-07-21 10:44:17 +09:00
jade
39513a1470 feat: apply full spec to ShinhanCommonHeaderDto (42 fields) 2026-07-21 10:36:34 +09:00
jade
821b249d50 fix: resolve MCI sample tool execution errors and improve parameter definitions in UI 2026-07-21 10:29:30 +09:00
jade
e02a4e24e8 fix: Add dap-tool-core to dap-gateway Dockerfile 2026-07-21 09:06:34 +09:00
jade
6b8ca5581f fix: Remove COPY dap-common from Dockerfiles 2026-07-21 09:04:07 +09:00
jade
d6869f8191 refactor: Merge dap-common into dap-tool-core 2026-07-21 09:02:48 +09:00
jade
515ab409ec fix: Update BusinessToolController to use generateSchema 2026-07-20 17:15:49 +09:00
jade
b55f863de9 fix: Complete JsonSchemaGenerator with required properties for MCP compliance 2026-07-20 17:07:39 +09:00
jade
80ff392b5a feat: Add SampleMciToolService for MCI integration sample 2026-07-20 16:44:12 +09:00
123 changed files with 1252 additions and 324 deletions

View File

@@ -1,30 +1,30 @@
# 1. 鍮뚮뱶 ?섍꼍 (JDK 21)
# 1. ??š®ë±???<3F>ê¼<C3AA> (JDK 21)
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
# Gradle ?섑띁?€ ?뚯뒪 蹂듭궗
# Gradle ??ë<E28098><C3AB>?? ???’ª 蹂듭ê¶?
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY src src
# 沅뚰븳 遺€??諛?鍮뚮뱶 (?뚯뒪???쒖쇅)
# æ²…ëš°ë¸??ºÂ€??è«???š®ë±?(???’ª????–쇅)
RUN chmod +x gradlew
RUN ./gradlew clean build -x test
# 2. ?ㅽ뻾 ?섍꼍 (JRE 21)
# 2. ??½ë»¾ ??<3F>ê¼<C3AA> (JRE 21)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# ?€?꾩〈 ?ㅼ젙 (?쒓뎅 ?쒓컙)
# ???꾩ã€???¼ì ™ (??“뎅 ??“ì»™)
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
# 鍮뚮뱶??JAR ?뚯씪 蹂듭궗
# ??š®ë±??JAR ???”ª 蹂듭ê¶?
COPY --from=builder /app/build/libs/*.jar app.jar
# 湲곕낯 ?ы듃 ?몄텧
# 湲곕???????몄텧
EXPOSE 8081
# 而⑦뀒?대꼫 ?ㅽ뻾 ??JAR ?ㅽ뻾
# ?Œâ‘¦???€ê¼???½ë»¾ ??JAR ??½ë»¾
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -45,6 +45,7 @@ DAP Backend는 2개의 주요 애플리케이션으로 분리 운영됩니다:
- `HR`: 휴가 등록, 연차 갯수 조회
- `CONTRACT`: 계약 상태, 계약 상세 조회
- `CUSTOMER`: 고객 등급, 고객 상세 정보 조회
- `SAMPLE`: 날씨, 환율, 명언 조회 등 외부 연동 샘플
- IntelliJ IDEA: `Run/Debug Configurations`에서 `DapTool*Application``Program arguments``--mcp.tool.target=NOTIFICATION` 입력
@@ -66,7 +67,7 @@ DAP Backend는 2개의 주요 애플리케이션으로 분리 운영됩니다:
}
}
```
- **특정 카테고리 툴 필터링**: `McpBridge.java` 내부의 URI 파라미터(`?categoryKey=common`)를 수정하여 원하는 도메인의 툴만 선택적으로 AI에게 학습시킬 수 있습니다.
- **특정 카테고리 툴 필터링**: `McpBridge.java` 내부의 URI 파라미터(`?categoryKey=sample` 등)를 수정하여 원하는 도메인의 툴만 선택적으로 AI에게 학습시킬 수 있습니다.
### 2. 프로덕션 클라우드 AI (Google Cloud Agent Builder 등) 연동
실제 라이브 서비스에서 동작하는 클라우드 Agent Builder는 REST API 기반의 OpenAPI Spec을 요구합니다.

View File

@@ -1,35 +0,0 @@
plugins {
id 'java-library'
}
dependencies {
api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-validation'
api 'org.springframework.boot:spring-boot-starter-data-redis'
api 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
api 'io.github.resilience4j:resilience4j-core:2.2.0'
api 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0'
api 'io.github.resilience4j:resilience4j-ratelimiter'
api 'io.github.resilience4j:resilience4j-retry:2.2.0'
api 'org.springframework.boot:spring-boot-starter-aop:3.3.0'
api 'org.springframework.boot:spring-boot-starter-jdbc'
api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
api 'com.h2database:h2'
api 'p6spy:p6spy:3.9.1'
api 'io.lettuce:lettuce-core:6.6.0.RELEASE'
api "org.mapstruct:mapstruct:1.5.5.Final"
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
api 'com.networknt:json-schema-validator:1.4.0'
api 'org.springframework.kafka:spring-kafka:3.2.0'
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}

View File

@@ -1,40 +0,0 @@
package io.shinhanlife.dap.common.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dap.common.integration.mci.dto
* @className ShinhanCommonHeaderDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShinhanCommonHeaderDto {
private String envrTypeCd; // 환경유형코드 (D, T, R)
private String glbId; // 전사공통키 (37 Byte)
private String pgrsSriaNo; // 진행일련번호
private String trgmVrsnInfoValu; // 전문버전정보값
private String tgrmEncrYn; // 전문암호화여부
private String gpcpCd; // 글로벌법인코드
private String appliDutjCd; // 어플리케이션업무코드
private String rcvSvcId; // 수신서비스ID
private String reqRspnScCd; // 요청응답구분코드 (S:요청, R:응답)
private String inqrTraTypeCd; // 조회거래유형코드
private String reqTgrmTnsmDtptDt; // 요청전문전송일시
private String itrIfId; // 인터페이스ID
}

View File

@@ -3,7 +3,8 @@ WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY dap-common dap-common
COPY dap-tool-core dap-tool-core
COPY dap-gateway dap-gateway
RUN chmod +x gradlew
RUN ./gradlew :dap-gateway:build -x test

View File

@@ -3,7 +3,7 @@ plugins {
}
dependencies {
implementation project(':dap-common')
implementation project(':dap-tool-core')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

View File

@@ -162,22 +162,53 @@ public class ExecuteService {
long elapsedMillis = elapsedMillis(startedAt);
String responseText = "";
try { responseText = objectMapper.writeValueAsString(result); } catch (Exception ignore) {}
long originalSize = 0;
try {
responseText = objectMapper.writeValueAsString(result);
originalSize = responseText.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
} catch (Exception ignore) {}
Map<String, Object> finalResult = new java.util.LinkedHashMap<>();
finalResult.put("status", "ok");
finalResult.put("result", result);
finalResult.put("error_code", null);
finalResult.put("error_message", null);
finalResult.put("elapsed_ms", elapsedMillis);
finalResult.put("truncated", false);
finalResult.put("original_size", originalSize);
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
return result;
return finalResult;
} catch (ToolExecutionException error) {
long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
throw error;
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
errorResult.put("result", null);
errorResult.put("error_code", error.failureType().name());
errorResult.put("error_message", error.getMessage());
errorResult.put("elapsed_ms", elapsedMillis);
errorResult.put("truncated", false);
errorResult.put("original_size", 0);
return errorResult;
} catch (Exception error) {
long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + toolName, error);
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
errorResult.put("result", null);
errorResult.put("error_code", FailureType.INTERNAL_ERROR.name());
errorResult.put("error_message", error.getMessage());
errorResult.put("elapsed_ms", elapsedMillis);
errorResult.put("truncated", false);
errorResult.put("original_size", 0);
return errorResult;
}
}
@@ -230,20 +261,17 @@ public class ExecuteService {
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
targetUrl = metadata.getPodUrl();
}
String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call";
String executeApiUrl = targetUrl + "/mcp/" + metadata.getName();
Map<String, String> headers = new java.util.HashMap<>();
headers.put("trace-id", context.requestId());
headers.put("request-id", java.util.UUID.randomUUID().toString());
ObjectNode pageArguments = paginationValidator.normalize(arguments);
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
while (true) {
Map<String, Object> pagePayload = new HashMap<>(payload);
if (pagePayload.containsKey("params")) {
Map<String, Object> params = new HashMap<>((Map<String, Object>) pagePayload.get("params"));
params.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
pagePayload.put("params", params);
} else {
pagePayload.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
}
Map<String, Object> pagePayload = objectMapper.convertValue(pageArguments, Map.class);
try {
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload))));
@@ -251,13 +279,16 @@ public class ExecuteService {
JsonNode data = null;
try {
data = toolInvoker.invoke(pagePayload, executeApiUrl);
data = toolInvoker.invoke(pagePayload, executeApiUrl, headers);
} catch (org.springframework.web.client.RestClientResponseException e) {
// HTTP 4xx, 5xx 에러는 연결 오류가 아니라 비즈니스 로직 오류이거나 검증 실패이므로 원본 에러를 그대로 반환
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 에러 (" + e.getStatusCode() + "): " + e.getResponseBodyAsString());
} catch (Exception e) {
if (executeApiUrl.contains("http://tool-")) {
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
try {
data = toolInvoker.invoke(pagePayload, fallbackUrl);
data = toolInvoker.invoke(pagePayload, fallbackUrl, headers);
} catch (Exception ex) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
}

View File

@@ -54,6 +54,7 @@ public class DynamicMcpServerManager {
getOrCreateServer("sms");
getOrCreateServer("email");
getOrCreateServer("other");
getOrCreateServer("sample");
}
private McpSyncServer getOrCreateServer(String categoryKey) {

View File

@@ -40,14 +40,17 @@ public class HttpToolInvoker implements ToolInvoker {
}
@Override
public JsonNode invoke(Map<String, Object> payload, String targetUrl) {
public JsonNode invoke(Map<String, Object> payload, String targetUrl, Map<String, String> headers) {
try {
Object httpResult = restClient.post()
RestClient.RequestBodySpec requestSpec = restClient.post()
.uri(targetUrl)
.contentType(MediaType.APPLICATION_JSON)
// TODO: Use actual tenant's key
.header("X-Trace-Id", UUID.randomUUID().toString())
.body(payload)
.contentType(MediaType.APPLICATION_JSON);
if (headers != null) {
headers.forEach(requestSpec::header);
}
Object httpResult = requestSpec.body(payload)
.retrieve()
.body(Object.class);

View File

@@ -22,5 +22,5 @@ import java.util.Map;
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
*/
public interface ToolInvoker {
JsonNode invoke(Map<String, Object> payload, String targetUrl);
JsonNode invoke(Map<String, Object> payload, String targetUrl, Map<String, String> headers);
}

View File

@@ -1,17 +1,17 @@
# Dev 환경 전용 설정 (개발 서버 DB 연결 정보 등)
# Dev ?瞘祭 ?<3F>鹻 ?木<> (穈嶅<E7A988> ?嶅<> DB ?國盒 ?瑅陷 ??
# spring.datasource.url=jdbc:oracle:thin:@//dev-db-host:1521/XEPDB1
# spring.datasource.driverClassName=oracle.jdbc.OracleDriver
# spring.datasource.username=dev_user
# spring.datasource.password=dev_password
# Render 클라우드 환경 전용 Fallback 라우팅
# Render ?渠𦉘?圉<> ?瞘祭 ?<3F> Fallback ?潰黱??
mcp.gateway.fallback.routes.sms=https://dap-tool-sms.onrender.com
mcp.gateway.fallback.routes.email=https://dap-tool-email.onrender.com
mcp.gateway.fallback.default-url=https://dap-tool-other.onrender.com
mcp.gateway.fallback.routes.payment=https://dap-tool-payment.onrender.com
mcp.gateway.fallback.routes.hr=https://dap-tool-hr.onrender.com
# --- 신한라이프 EAI/MCI 연계 IP 정보 (개발 환경) ---
# --- ?𡥄<>?潰𦚯??EAI/MCI ?國<> IP ?瑅陷 (穈嶅<E7A988> ?瞘祭) ---
shinhan.integration.envrTypeCd=D
shinhan.integration.eai.url=http://10.176.32.181
shinhan.integration.internalMci.url=http://10.176.32.173

View File

@@ -464,6 +464,7 @@
<label class="form-label">Domain Category</label>
<input type="text" class="form-control" name="categoryKey" pattern="^[a-z0-9][a-z0-9_-]*$" list="groupList" placeholder="e.g. common, hr, payment" oninput="this.value = this.value.toLowerCase()" required>
<datalist id="groupList">
<option value="sample">sample</option>
<option value="common">common</option>
<option value="customer">customer</option>
<option value="contract">contract</option>

View File

@@ -3,7 +3,30 @@ plugins {
}
dependencies {
api project(':dap-common')
api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-validation'
api 'org.springframework.boot:spring-boot-starter-data-redis'
api 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
api 'io.github.resilience4j:resilience4j-core:2.2.0'
api 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0'
api 'io.github.resilience4j:resilience4j-ratelimiter'
api 'io.github.resilience4j:resilience4j-retry:2.2.0'
api 'org.springframework.boot:spring-boot-starter-aop:3.3.0'
api 'org.springframework.boot:spring-boot-starter-jdbc'
api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
api 'com.h2database:h2'
api 'p6spy:p6spy:3.9.1'
api 'io.lettuce:lettuce-core:6.6.0.RELEASE'
api "org.mapstruct:mapstruct:1.5.5.Final"
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
api 'com.networknt:json-schema-validator:1.4.0'
api 'org.springframework.kafka:spring-kafka:3.2.0'
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
}
dependencies {

View File

@@ -0,0 +1,39 @@
package io.shinhanlife.dap.common.integration.mci.component;
import io.shinhanlife.glow.communication.dto.Transfer;
import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 신한라이프 내부 Glow 표준 컴포넌트 어댑터
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AxhubMciComponent {
@SuppressWarnings("rawtypes")
private final GlowMciComponent mci;
@SuppressWarnings("unchecked")
public <O, I> Transfer<O> callTo(String itrfName, String rcvSvcId, I inputDto, Class<O> resBodyClass) {
log.info("[AxhubMciComponent] MCI 호출 준비 - 인터페이스: {}, 수신서비스: {}", itrfName, rcvSvcId);
// Header 세팅 로직 생략 (Mock)
Transfer<Object> request = Transfer.builder()
.body(inputDto)
.resBodyClass((Class<Object>) (Class<?>) resBodyClass)
.build();
return syncMci(request);
}
@SuppressWarnings("unchecked")
private <O> Transfer<O> syncMci(Transfer<Object> request) {
log.info("[AxhubMciComponent] GlowMciComponent.sync() 호출");
return (Transfer<O>) mci.sync(request);
}
}

View File

@@ -0,0 +1,19 @@
package io.shinhanlife.dap.common.integration.mci.config;
import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* TODO: 실제 Glow Framework 의존성이 추가되어 io.shinhanlife.glow 패키지가
* ComponentScan에 잡히게 되면 이 설정 클래스는 삭제하세요.
*/
@Configuration
public class GlowMockConfig {
@Bean
@SuppressWarnings("rawtypes")
public GlowMciComponent glowMciComponent() {
return new GlowMciComponent();
}
}

View File

@@ -0,0 +1,90 @@
package io.shinhanlife.dap.common.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @package io.shinhanlife.dap.common.integration.mci.dto
* @className OlCommonHeaderDto
* @description AX HUB 시스템 처리 클래스 - OL(구 오렌지라이프) 공통 헤더
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OlCommonHeaderDto {
private String custNm; // 고객명
private String custRrn; // 고객 주민등록번호
private String custNo; // 고객번호
private String rcevNo; // 접수번호
private String pono; // 증권번호
private String scrNm; // 화면명
private String scrId; // 화면ID
private String lginDttm; // 사용자가 로그인한 접속일시
private String lginIpAddr; // 사용자가 접속한 IP 주소
private String userNm; // 사용자 이름(한글)
private String userEngNm; // 사용자 영문이름
private String userId; // 사용자 ID(AD ID)
private String userNo; // 사용자번호
private String deptCd; // 사용자조직 코드
private String salsDvCd; // 영업본부코드
private String salsBoCd; // 영업지점코드
private String uppDeptCd; // 상위조직코드
private String prcsrUserId; // 처리자 ID(AD ID)
private String prcsrUserNo; // 처리지번호
private String prcsrDeptCd; // 처리지조직 코드
private String prcsrDvCd; // 처리지 영업본부코드
private String prcsrBoCd; // 처리지 영업지점코드
private String prcsrUppDeptCd; // 부서코드
private String sysCd; // 요청이 들어온 시스템을 표시
private String reqtSvcNm; // 요청하는 서비스 모듈명
private String reqtMthdNm; // 요청하는 메소드명
private String reqtVoNm; // 요청메소드에 전달할 값을 담는 VO명
private String scrButnFuncClssCd; // 화면에서 버튼 별 이벤트 구분을 위한 구분코드
private String scrGriCnt; // 화면 그리드 개수
private List<OlPageDto> pageList; // 페이징 리스트 (L2 반복)
private String reqtDttm; // 요청일시
private String crdtInfoIcluFlg; // 신용정보포함여부(Y,N)
private String crdtInfoDataChgTypCd; // 업무내역별 식별코드 부여
private String crdtInfoIdfInEngAbbrNm; // 신용정보식별영문약어명
private String crdtInfoIdfnSysCd; // 신용정보식별시스템코드
private String scrButnNm; // 화면버튼명
private String msgCnt; // 메시지 개수
private List<OlMsgDto> msgList; // 메시지 리스트 (L2 반복)
private String respDttm; // 응답일시
private String svcRunNm; // 거래별로 유일한 ServiceExecutionID
private String stdate; // 기준일자
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class OlPageDto {
private String pageSrno; // 페이지 인덱스값 (L3)
private String pageInqCnt; // 한페이지에 조회될 건수 (L3)
private String nxtButnNm; // 다음버튼ID (L3)
private String nxtButnEnbFlg; // 다음버튼 활성여부 (L3)
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class OlMsgDto {
private String msgNo; // 서버 측에서 세팅한 정상/에러 메시지코드 (L3)
private String msgTypCd; // 메시지유형코드 (L3)
private String msgNm; // 메시지코드의 내용 (L3)
}
}

View File

@@ -0,0 +1,72 @@
package io.shinhanlife.dap.common.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dap.common.integration.mci.dto
* @className ShinhanCommonHeaderDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShinhanCommonHeaderDto {
private String tgrmLencn; // 전문길이
private String glbId; // 글로벌ID (전사공통키)
private String pgrsSriaNo; // 진행일련번호
private String tgrmVrsnInfoValu; // 전문버전정보값
private String tgrmEncrYn; // 전문암호화여부
private String gpcpCd; // 그룹사코드
private String appliDutjCd; // 어플리케이션업무코드
private String appliDtptDutjCd; // 어플리케이션상세업무코드
private String frbuCd; // 대외기관코드
private String cmouDutjCd; // 대외업무코드
private String cmouCssfCd; // 대외종별코드
private String cmouTraCd; // 대외거래코드
private String rcvSvcId; // 수신서비스ID
private String rsltRcvSvcId; // 결과수신서비스ID
private String tgrmCreaChnnTypeCd; // 전문생성채널유형코드
private String lnggDvsnCd; // 언어구분코드
private String simulTraYn; // 시뮬레이션거래여부
private String itrIfId; // 인터페이스ID
private String reqRspnScCd; // 요청응답구분코드
private String tnsmTypeCd; // 전송유형코드
private String envrTypeCd; // 환경유형코드
private String inqrTraTypeCd; // 조회거래유형코드
private String reqTgrmTnsmDtptDt; // 요청전문전송상세일시
private String strYmd; // 기준일자
private String scrnId; // 화면ID
private String scrnBtnId; // 화면버튼ID
private String userIpAddr; // 사용자IP주소
private String drtmCd; // 부서코드
private String userId; // 사용자ID
private String indvCtinRoleCd; // 개인신용정보역할코드
private String acntOgnzNo; // 경리조직번호
private String rspnTgrmTnsmDtptDt; // 응답전문전송상세일시
private String tgrmDalRsltCd; // 전문처리결과코드
private String ognzAsrtCd; // 조직분류코드
private String ognzLeveCd; // 조직레벨코드
private String psmrAsrtCd; // 인사조직분류코드
private String sbsnRulpAsrtCd; // 영업규정분류코드
private String bsduCd; // 영업지국코드
private String bsquCd; // 영업자격코드
private String linkPrafDutyCd; // 연계인사직책코드
private String indvInfoLogWritYn; // 개인정보로그작성여부
private String prepImhdNm; // 예비항목명
}

View File

@@ -0,0 +1,51 @@
package io.shinhanlife.dap.common.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dap.common.integration.mci.dto
* @className ShinhanMessageDto
* @description AX HUB 시스템 처리 클래스 - MCI 전문 메시지부
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShinhanMessageDto {
private MsgHddvValu msgHddvValu; // 메시지헤더부값
private MsgDtdvValu msgDtdvValu; // 메시지데이터부값
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class MsgHddvValu {
private String msgTnsmTypeCd; // 메시지전송유형코드
private Integer msdvLencn; // 메시지부길이
private Integer msgRpttCc; // 메시지반복건수
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class MsgDtdvValu {
private String msgCd; // 메시지코드
private String msgPrnAttrCd; // 메시지출력속성코드
private String msgCt; // 메시지내용
private String anxMsgCt; // 부가메시지내용
}
}

View File

@@ -0,0 +1,38 @@
package io.shinhanlife.dap.common.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
/**
* @package io.shinhanlife.dap.common.integration.mci.dto
* @className ShinhanTelegramWrapper
* @description AX HUB 시스템 처리 클래스 - MCI 전문 전체 래퍼 (공통헤더부 + 메시지부 + 데이터부)
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShinhanTelegramWrapper<T> {
// 1. 공통 헤더부
private ShinhanCommonHeaderDto tgrmCmnnhddValu;
// 2. 메시지부
private ShinhanMessageDto tgrmMsdvValu;
// 3. 데이터부 (비즈니스마다 다름, JsonUnwrapped로 평탄화하거나 객체 자체로 유지 가능. 여기서는 객체 유지)
private T tgrmDtdvValu;
}

View File

@@ -0,0 +1,84 @@
package io.shinhanlife.dap.common.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dap.common.integration.mci.dto
* @className SlCommonHeaderDto
* @description AX HUB 시스템 처리 클래스 - SL(신한라이프) 표준 헤더
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SlCommonHeaderDto {
private String length; // 전문길이
private SlGlobalId globalId; // 글로벌ID
private String headerVer; // 전문헤더버전
private String encodeFlag; // 전문암호화여부
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SlGlobalId {
private String writeDate; // 전문작성일 (8)
private String sysCd; // 생성시스템명 (8)
private String typeCd; // 구분코드 (2)
private String detailCd; // 세부업무코드 (4)
private String seqNo; // 채번번호 (8)
private String step; // 진행상황 (2)
}
private String groupCoCd; // 그룹사코드
private String instCd; // 기관코드
private String applCd; // 업무코드
private String kindCd; // 종별코드
private String txCd; // 거래코드
private String pfmAppName; // 어플리케이션 명
private String pfmSvcName; // 서비스 명
private String pfmFnName; // 오퍼레이션 명
private String systemCd; // 생성시스템구분
private String trFlag; // 요청응답구분
private String syncFlag; // 동기구분
private String envrFlag; // 환경구분
private String crudFlag; // 조회거래구분
private String sendTime; // 전문전송일시
private String screenId; // 화면ID
private String clntIp; // Client IP
private String orgCd; // 부서(지점)코드
private String userId; // 사용자 사번(아이디)
private String indvCrdtInfo; // 개인신용정보역할코드
private String acntOgnzNo; // 경리조직번호
private String ttiFlag; // TimeOut사용
private String ttiStartTm; // 최초시작시간
private String ttiKeepTm; // 유지시간초수
private String outMsgTm; // 응답전문작성일시
private String resType; // 처리결과
private String resCode; // 응답코드
private String resBascMsg; // 응답기본내역
private String msgType; // 메시지 유형
private String rcvSvcCd; // 수신 서비스 Code
private String rsltRcvSvcCd; // 결과수신 서비스 Code
private String realSvcCd; // Real 서비스 Code
private String ognzAsrtCd; // 조직분류코드
private String ognzLeveCd; // 조직레벨구분코드
private String psmrAsrtCd; // 인사조직분류코드
private String sbsnRulpAsrtCd; // 영업규정분류코드
private String bsduCd; // 영업지국코드
private String bsquCd; // 영업자격코드
private String linkPrafDutyCd; // 직책코드
private String temp; // 예비 필드
}

View File

@@ -106,8 +106,8 @@ public class PodScaffolder {
*
* </pre>
*/
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config", "io.shinhanlife.dap.common.integration"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config", "io.shinhanlife.dap.common.integration"})
@EnableCaching
public class DapTool%sApplication {
public static void main(String[] args) {
@@ -135,12 +135,12 @@ public class PodScaffolder {
# Suppress Kafka Connection Logs
logging.level.org.apache.kafka=ERROR
# Auto Prefix Namespace
mcp.namespace=%s
# Auto Prefix Namespace (Disabled by default)
mcp.namespace=
# API 보안 설정
mcp.security.tenant-domains.TESTER-DEV=ALL
""".formatted(portStr, moduleName, shortName);
""".formatted(portStr, moduleName);
Files.writeString(resPath.resolve("application.properties"), applicationProperties);
String applicationLocalProperties = """
@@ -163,7 +163,7 @@ public class PodScaffolder {
# Gateway/Tool URLs
axhub.gateway.url=http://localhost:8081
axhub.tool.url=http://localhost:${server.port}
axhub.tool.url=${AXHUB_TOOL_URL:http://localhost:${server.port}}
# Disable Kafka
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration

View File

@@ -49,7 +49,7 @@ public class ToolScaffolder {
String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: ");
String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): ");
String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): ");
String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): ");
String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: SAMPLE, NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): ");
if (group.isEmpty()) group = "COMMON";
String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): ");
if (routingType.trim().isEmpty()) {

View File

@@ -31,7 +31,7 @@ public @interface McpFunction {
String inputSchema() default "{}";
// 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어
boolean register() default true;
boolean register() default false;
// 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김)
boolean visible() default true;

View File

@@ -65,26 +65,26 @@ public class BusinessToolController {
return toolRegistryHeartbeatSender.getAllScannedTools();
}
// JSON RPC 기반 단일 라우팅 엔드포인트
@PostMapping("/mcp/api/v1/tools/call")
public ResponseEntity<Map<String, Object>> executeDynamicTool(
// 순수 REST 기반 동적 라우팅 엔드포인트
@PostMapping("/mcp/{name}")
public ResponseEntity<?> executeDynamicTool(
@PathVariable("name") String functionName,
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId,
@RequestBody(required = false) Map<String, Object> payload) {
@RequestHeader(value = "trace-id", required = false) String traceId,
@RequestHeader(value = "request-id", required = false) String requestId,
@RequestBody(required = false) Map<String, Object> arguments) {
String finalRequestId = headerRequestId != null ? headerRequestId : (payload != null && payload.get("id") != null ? String.valueOf(payload.get("id")) : null);
Map<String, Object> params = payload != null ? (Map<String, Object>) payload.get("params") : null;
String functionName = params != null ? (String) params.get("name") : null;
String finalRequestId = headerRequestId;
log.info("\n [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName);
if (payload != null) {
log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId);
log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName);
if (arguments != null) {
try {
log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(payload));
log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments));
} catch (Exception e) {
log.info(" [Tool] 호출 파라미터: {}", payload);
log.info(" [Tool] 호출 파라미터: {}", arguments);
}
}
Map<String, Object> arguments = params != null ? (Map<String, Object>) params.get("arguments") : null;
Object targetBean = null;
Method targetMethod = null;
McpFunction targetFunctionAnnotation = null;
@@ -135,11 +135,7 @@ public class BusinessToolController {
errorBody.put("details", errorDetails);
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
Map<String, Object> error = new HashMap<>();
error.put("jsonrpc", "2.0");
error.put("error", errorBody);
error.put("id", payload != null ? payload.get("id") : null);
return ResponseEntity.status(404).body(error);
return ResponseEntity.status(404).body(errorBody);
}
// (기존 차단 로직 제거됨)
@@ -148,8 +144,8 @@ public class BusinessToolController {
Class<?> paramType = targetMethod.getParameterTypes()[0];
if (!Map.class.isAssignableFrom(paramType)) {
try {
Map<String, Object> autoSchema = JsonSchemaGenerator.generatePropertiesSchema(paramType);
String fullSchemaJson = "{\"type\":\"object\", \"properties\":" + objectMapper.writeValueAsString(autoSchema) + "}";
Map<String, Object> autoSchema = JsonSchemaGenerator.generateSchema(paramType);
String fullSchemaJson = objectMapper.writeValueAsString(autoSchema);
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
JsonSchema schema = factory.getSchema(fullSchemaJson);
@@ -169,11 +165,7 @@ public class BusinessToolController {
errorBody.put("details", errorDetails);
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
Map<String, Object> error = new HashMap<>();
error.put("jsonrpc", "2.0");
error.put("error", errorBody);
error.put("id", payload != null ? payload.get("id") : null);
return ResponseEntity.status(422).body(error);
return ResponseEntity.status(422).body(errorBody);
}
} catch (Exception e) {
log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage());
@@ -202,60 +194,20 @@ public class BusinessToolController {
long elapsed = System.currentTimeMillis() - startTime;
// 5. 결과 조립 (JSON-RPC 응답 - Agent Builder 규격 적용)
Map<String, Object> innerResult = new HashMap<>();
if (methodResult instanceof Map && ((Map<?, ?>) methodResult).containsKey("contracts")) {
innerResult.putAll((Map<String, Object>) methodResult);
} else {
List<Object> contracts = new ArrayList<>();
if (methodResult != null) {
contracts.add(methodResult);
}
innerResult.put("contracts", contracts);
if (methodResult instanceof Map && ((Map<?, ?>) methodResult).containsKey("status")) {
innerResult.put("status", ((Map<?, ?>) methodResult).get("status"));
} else {
innerResult.put("status", "SUCCESS");
}
}
Map<String, Object> resultPayload = new HashMap<>();
resultPayload.put("status", "ok");
resultPayload.put("result", innerResult);
resultPayload.put("error_code", null);
resultPayload.put("error_message", null);
resultPayload.put("elapsed_ms", elapsed);
resultPayload.put("truncated", false);
int originalSize = 0;
// 5. 결과 반환 (순수 REST 응답)
try {
if (methodResult instanceof Map && ((Map<?, ?>) methodResult).containsKey("legacy_response")) {
Object legacyResp = ((Map<?, ?>) methodResult).get("legacy_response");
if (legacyResp instanceof String) {
originalSize = ((String) legacyResp).getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
}
} else {
String jsonStr = objectMapper.writeValueAsString(innerResult);
originalSize = jsonStr.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
}
log.info("[Tool -> MCP Gateway] 동적 툴 실행 결과 반환: {}", objectMapper.writeValueAsString(methodResult));
} catch (Exception e) {
log.warn("Failed to calculate original_size", e);
}
resultPayload.put("original_size", originalSize);
Map<String, Object> rpcResponse = new LinkedHashMap<>(); // 순서 보장을 위해 LinkedHashMap 사용
rpcResponse.put("jsonrpc", "2.0");
rpcResponse.put("result", resultPayload);
rpcResponse.put("id", payload != null ? payload.get("id") : null);
try {
log.info("[Tool -> MCP Gateway] 동적 툴 실행 결과 반환: {}", objectMapper.writeValueAsString(rpcResponse));
} catch (Exception e) {
log.info("[Tool -> MCP Gateway] 동적 툴 실행 결과 반환: {}", rpcResponse);
log.info("[Tool -> MCP Gateway] 동적 툴 실행 결과 반환: {}", methodResult);
}
log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId);
return ResponseEntity.ok(rpcResponse);
ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok();
if (traceId != null) responseBuilder.header("trace-id", traceId);
if (requestId != null) responseBuilder.header("request-id", requestId);
return responseBuilder.body(methodResult);
} catch (Exception e) {
log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage());
@@ -268,11 +220,7 @@ public class BusinessToolController {
errorBody.put("details", errorDetails);
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
Map<String, Object> error = new HashMap<>();
error.put("jsonrpc", "2.0");
error.put("error", errorBody);
error.put("id", payload != null ? payload.get("id") : null);
return ResponseEntity.status(502).body(error);
return ResponseEntity.status(502).body(errorBody);
}
}
}

View File

@@ -120,10 +120,7 @@ public class ToolRegistryHeartbeatSender {
if (method.getParameterCount() > 0) {
try {
Class<?> paramType = method.getParameterTypes()[0];
Map<String, Object> schema = JsonSchemaGenerator.generatePropertiesSchema(paramType);
Map<String, Object> finalSchema = new HashMap<>();
finalSchema.put("type", "object");
finalSchema.put("properties", schema);
Map<String, Object> finalSchema = JsonSchemaGenerator.generateSchema(paramType);
meta.setParametersSchema(finalSchema);
} catch (Exception e) {
log.error("Failed to generate schema for {}", subToolName, e);

View File

@@ -1,62 +1,68 @@
package io.shinhanlife.dap.mcc.util;
/**
* @package io.shinhanlife.dap.mcc.util
* @className JsonSchemaGenerator
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcc.annotation.McpParameter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonSchemaGenerator {
/**
* Java DTO 클래스를 분석하여 MCP 규격의 JSON Schema (Properties)를 생성합니다.
*/
public static Map<String, Object> generatePropertiesSchema(Class<?> clazz) {
Map<String, Object> properties = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
Map<String, Object> fieldSchema = new HashMap<>();
// 1. 타입 매핑
fieldSchema.put("type", mapJavaTypeToJsonType(field.getType()));
// 2. 어노테이션 기반 설명 추출
McpParameter paramAnnotation = field.getAnnotation(McpParameter.class);
if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
fieldSchema.put("description", paramAnnotation.description());
} else {
fieldSchema.put("description", field.getName()); // 기본값
}
properties.put(field.getName(), fieldSchema);
}
return properties;
}
private static String mapJavaTypeToJsonType(Class<?> clazz) {
if (clazz == String.class) return "string";
if (clazz == Integer.class || clazz == int.class) return "integer";
if (clazz == Long.class || clazz == long.class) return "integer";
if (clazz == Double.class || clazz == double.class) return "number";
if (clazz == Float.class || clazz == float.class) return "number";
if (clazz == Boolean.class || clazz == boolean.class) return "boolean";
if (List.class.isAssignableFrom(clazz)) return "array";
return "object";
}
}
package io.shinhanlife.dap.mcc.util;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.shinhanlife.dap.mcc.annotation.McpParameter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonSchemaGenerator {
/**
* Java DTO 클래스를 분석하여 MCP 규격의 완전한 JSON Schema를 생성합니다.
*/
public static Map<String, Object> generateSchema(Class<?> clazz) {
Map<String, Object> schema = new HashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new HashMap<>();
List<String> requiredList = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
Map<String, Object> fieldSchema = new HashMap<>();
// 1. 타입 매핑
fieldSchema.put("type", mapJavaTypeToJsonType(field.getType()));
// 2. 어노테이션 기반 설명 추출
McpParameter paramAnnotation = field.getAnnotation(McpParameter.class);
JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class);
if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
fieldSchema.put("description", paramAnnotation.description());
} else if (descAnnotation != null && !descAnnotation.value().isEmpty()) {
fieldSchema.put("description", descAnnotation.value());
} else {
fieldSchema.put("description", field.getName()); // 기본값
}
// 3. 필수 여부 판단
JsonProperty jsonProp = field.getAnnotation(JsonProperty.class);
if ((jsonProp != null && jsonProp.required()) || (paramAnnotation != null && paramAnnotation.required())) {
requiredList.add(field.getName());
}
properties.put(field.getName(), fieldSchema);
}
schema.put("properties", properties);
if (!requiredList.isEmpty()) {
schema.put("required", requiredList);
}
return schema;
}
private static String mapJavaTypeToJsonType(Class<?> clazz) {
if (clazz == String.class) return "string";
if (clazz == Integer.class || clazz == int.class) return "integer";
if (clazz == Long.class || clazz == long.class) return "integer";
if (clazz == Double.class || clazz == double.class) return "number";
if (clazz == Float.class || clazz == float.class) return "number";
if (clazz == Boolean.class || clazz == boolean.class) return "boolean";
if (List.class.isAssignableFrom(clazz)) return "array";
return "object";
}
}

View File

@@ -0,0 +1,20 @@
package io.shinhanlife.glow.communication.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* TODO: 실제 Glow Framework 의존성(JAR)이 추가되면 이 Mock 클래스를 삭제하세요.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Transfer<T> {
private Object header;
private T body;
private Class<T> resBodyClass;
private Object message;
}

View File

@@ -0,0 +1,18 @@
package io.shinhanlife.glow.communication.module.mci.component;
import io.shinhanlife.glow.communication.dto.Transfer;
import org.springframework.stereotype.Component;
/**
* TODO: 실제 Glow Framework 의존성(JAR)이 추가되면 이 Mock 클래스를 삭제하세요.
*/
@Component
public class GlowMciComponent<S, R> {
public Transfer<R> sync(Transfer<S> request) {
// 실제 Glow HTTP 통신 (GlowHttpHeaderUtil, HttpClient 등) 수행 시뮬레이션
return (Transfer<R>) Transfer.builder()
.body(new Object())
.build();
}
}

View File

@@ -3,7 +3,7 @@ WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY dap-common dap-common
COPY dap-tool-core dap-tool-core
COPY dap-tool-email dap-tool-email
RUN chmod +x gradlew

View File

@@ -20,8 +20,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config", "io.shinhanlife.dap.common.integration"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config", "io.shinhanlife.dap.common.integration"})
@EnableCaching
public class DapToolEmailApplication {
public static void main(String[] args) {

View File

@@ -26,7 +26,7 @@ import java.util.Map;
@McpTool(routingType = "EAI", categoryKey = "notification")
public class EmailToolService extends AbstractMcpToolService {
@McpFunction(displayName = "send_email 툴", name = "send_email", description = "이메일 발송", prompt = "고객에게 이메일을 발송해줘.", mappingId = "EMAIL_SEND_001")
@McpFunction(register = false, displayName = "send_email 툴", name = "send_email", description = "이메일 발송", prompt = "고객에게 이메일을 발송해줘.", mappingId = "EMAIL_SEND_001")
public Object sendEmail(EmailSendReq req) {
log.info("[Email] 이메일 발송 요청 수신. 수신자: {}", req.getEmailAddress());

View File

@@ -1,9 +1,9 @@
# Render 클라우드 환경 전용 설정
# Render ?渠𦉘?圉<> ?瞘祭 ?<3F>鹻 ?木<>
server.port=${PORT:8083}
axhub.gateway.url=https://dap-gateway.onrender.com
axhub.gateway.url=https://axhub-gateway.onrender.com
axhub.tool.url=https://dap-tool-email.onrender.com
# EIMS 동적 라우팅 접속 정보 (Mock)
# EIMS ?軤<> ?潰黱???𡢾<> ?瑅陷 (Mock)
eims.http.url=http://localhost:${server.port}/api/gateway
eims.tcp.host=127.0.0.1
eims.tcp.port=8090
@@ -13,7 +13,7 @@ eims.jsp.json.url=http://localhost:${server.port}/mock/jsp-json
eims.mci.url=http://localhost:${server.port}/api/mock/esb/api
eims.mcistring.url=http://localhost:${server.port}/api/mock/esb/string
# --- 신한라이프 EAI/MCI 연계 IP 정보 (개발 환경) ---
# --- ?𡥄<>?潰𦚯??EAI/MCI ?國<> IP ?瑅陷 (穈嶅<E7A988> ?瞘祭) ---
shinhan.integration.envrTypeCd=D
shinhan.integration.eai.url=http://10.176.32.181
shinhan.integration.internalMci.url=http://10.176.32.173

View File

@@ -17,7 +17,7 @@ eims.mcistring.url=http://localhost:${server.port}/api/mock/esb/string
# Gateway/Tool URLs
axhub.gateway.url=http://localhost:8081
axhub.tool.url=http://localhost:${server.port}
axhub.tool.url=${AXHUB_TOOL_URL:http://localhost:${server.port}}
# Disable Kafka
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration

View File

@@ -7,7 +7,7 @@ spring.profiles.active=local
logging.level.org.apache.kafka=ERROR
# Auto Prefix Namespace
mcp.namespace=email
mcp.namespace=
# API 보안 키 설정
mcp.security.tenant-domains.TESTER-DEV=ALL

View File

@@ -3,7 +3,7 @@ WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY dap-common dap-common
COPY dap-tool-core dap-tool-core
COPY dap-tool-other dap-tool-other
RUN chmod +x gradlew

View File

@@ -0,0 +1,119 @@
package io.shinhanlife.dap.mcc.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.Data;
/**
* @package io.shinhanlife.dap.mcc.dto
* @className Onnba3011ReqDto
* @description 보종By가입설계한도계산조회 MCI 요청 전문 (ONNBA3011_I)
* @author 김형식
* @create 2026.09.01
*/
@Data
public class Onnba3011ReqDto {
@JsonPropertyDescription("통합기계약보험 (유형: gs, 길이: 72)")
@JsonProperty("unfcPrbuIrcoAddu")
private UnfcPrbuIrcoAdduDto unfcPrbuIrcoAddu;
@JsonPropertyDescription("처리구분코드 (길이: 1)")
@JsonProperty("dalScCd")
private String dalScCd;
@JsonPropertyDescription("고객청약관계 (길이: 2)")
@JsonProperty("cstSucoRltyCd")
private String cstSucoRltyCd;
@JsonPropertyDescription("고객번호 (길이: 12)")
@JsonProperty("csNo")
private String csNo;
@JsonPropertyDescription("주민등록번호 (길이: 50)")
@JsonProperty("rdreNo")
private String rdreNo;
@JsonPropertyDescription("통합급부계산 (길이: 1)")
@JsonProperty("unfcPvsCalReqYn")
private String unfcPvsCalReqYn;
@JsonPropertyDescription("한국신용정보 (길이: 1)")
@JsonProperty("kcisPymmTnnrReq")
private String kcisPymmTnnrReq;
@JsonPropertyDescription("한도초과여부 (길이: 1)")
@JsonProperty("lmovYn")
private String lmovYn;
@JsonPropertyDescription("일반경유승인 (길이: 1)")
@JsonProperty("genPsthApvTrgtYn")
private String genPsthApvTrgtYn;
@JsonPropertyDescription("보험사한도초과 (길이: 1)")
@JsonProperty("ircoLmovEcpbTrgtYn")
private String ircoLmovEcpbTrgtYn;
@JsonPropertyDescription("진단계산여부 (길이: 1)")
@JsonProperty("digCalYn")
private String digCalYn;
@JsonPropertyDescription("기계약포함진단 (길이: 1)")
@JsonProperty("prbuIciDigCalYn")
private String prbuIciDigCalYn;
@JsonPropertyDescription("청약심사기본Dto (유형: gs, 길이: 2532)")
@JsonProperty("sucoIspaBasDto")
private SucoIspaBasDto sucoIspaBasDto;
// ----- Nested DTO Classes -----
@Data
public static class UnfcPrbuIrcoAdduDto {
// 실제 필요한 하위 필드들 추가 (사진 생략부분)
}
@Data
public static class SucoIspaBasDto {
@JsonPropertyDescription("계약처리유형 (길이: 2)")
@JsonProperty("ccnDalTypCd")
private String ccnDalTypCd;
@JsonPropertyDescription("신계약입력경로 (길이: 2)")
@JsonProperty("nwcnptCursCd")
private String nwcnptCursCd;
@JsonPropertyDescription("개인단체계약 (길이: 2)")
@JsonProperty("induAsctScCd")
private String induAsctScCd;
@JsonPropertyDescription("모집조직번호 (길이: 7)")
@JsonProperty("cepeOgnzNo")
private String cepeOgnzNo;
@JsonPropertyDescription("모집자사번번호 (길이: 8)")
@JsonProperty("cepePrafNo")
private String cepePrafNo;
@JsonPropertyDescription("수금조직번호 (길이: 7)")
@JsonProperty("clmoOgnzNo")
private String clmoOgnzNo;
@JsonPropertyDescription("수금자사번번호 (길이: 8)")
@JsonProperty("clmoPrafNo")
private String clmoPrafNo;
@JsonPropertyDescription("청약일자 (길이: 20)")
@JsonProperty("sucoYmd")
private String sucoYmd;
@JsonPropertyDescription("발행일자 (길이: 20)")
@JsonProperty("ispDt")
private String ispDt;
@JsonPropertyDescription("계약일자 (길이: 20)")
@JsonProperty("contYmd")
private String contYmd;
}
}

View File

@@ -20,8 +20,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config", "io.shinhanlife.dap.common.integration", "io.shinhanlife.dap.other"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.common.adapter", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config", "io.shinhanlife.dap.common.integration", "io.shinhanlife.dap.other"})
@EnableCaching
public class DapToolOtherApplication {
public static void main(String[] args) {

View File

@@ -29,7 +29,7 @@ import java.util.Map;
@McpTool(routingType = "MCI", categoryKey = "common")
public class BalanceService extends AbstractMcpToolService {
@McpFunction(displayName = "balance 툴", name = "balance",
@McpFunction(register = false, displayName = "balance 툴", name = "balance",
description = "고객의 계좌 잔액을 조회합니다.",
prompt = "고객 계좌 잔액을 조회해줘.",
mappingId = "ACC_001"

View File

@@ -30,12 +30,12 @@ import io.shinhanlife.dap.mcc.dto.BillingProcessReq;
@lombok.extern.slf4j.Slf4j
public class BillingProcessService extends AbstractMcpToolService {
@McpFunction(displayName = "status 툴", name = "status", description = "청구심사 상태 조회", prompt = "현재 접수된 청구건 상태를 알려줘.", mappingId = "BILL_001")
@McpFunction(register = false, displayName = "status 툴", name = "status", description = "청구심사 상태 조회", prompt = "현재 접수된 청구건 상태를 알려줘.", mappingId = "BILL_001")
public Object getStatus(BillingStatusReq data) {
return executeBillingLogic("BILL_001", data);
}
@McpFunction(displayName = "process 툴", name = "process", description = "청구 처리", prompt = "현재 접수된 청구건에 대한 심사 처리를 진행해.", mappingId = "BILL_002")
@McpFunction(register = false, displayName = "process 툴", name = "process", description = "청구 처리", prompt = "현재 접수된 청구건에 대한 심사 처리를 진행해.", mappingId = "BILL_002")
public Object processBilling(BillingProcessReq data) {
return executeBillingLogic("BILL_002", data);
}

View File

@@ -25,12 +25,12 @@ import io.shinhanlife.dap.mcc.dto.BondIssueReq;
*/
public class BondIssueService extends AbstractMcpToolService {
@McpFunction(displayName = "check 툴", name = "check", register = true, description = "발행 가능 여부 조회 테스트", prompt = "디지털 증권 발행 한도가 충분한지 확인해줘.", mappingId = "BOND_001")
@McpFunction(displayName = "check 툴", name = "check", register = false, description = "발행 가능 여부 조회 테스트", prompt = "디지털 증권 발행 한도가 충분한지 확인해줘.", mappingId = "BOND_001")
public Object check(BondCheckReq data) {
return executeLegacy("EAI", "BOND_001", data);
}
@McpFunction(displayName = "issue 툴", name = "issue", register = true, description = "증권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002")
@McpFunction(displayName = "issue 툴", name = "issue", register = false, description = "증권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002")
public Object issue(BondIssueReq data) {
return executeLegacy("EAI", "BOND_002", data);
}

View File

@@ -26,17 +26,17 @@ import io.shinhanlife.dap.mcc.dto.LeaveCountReq;
*/
public class CommonUtilityService extends AbstractMcpToolService {
@McpFunction(displayName = "register_vacation 툴", name = "register_vacation", description = "휴가 등록", prompt = "내일 하루 연차 휴가를 등록해줘.", mappingId = "HR_VAC_01")
@McpFunction(register = false, displayName = "register_vacation 툴", name = "register_vacation", description = "휴가 등록", prompt = "내일 하루 연차 휴가를 등록해줘.", mappingId = "HR_VAC_01")
public Object registerVacation(VacationRegisterReq data) {
return executeLegacy("HTTP", "HR_VAC_01", data);
}
@McpFunction(displayName = "get_leave_count 툴", name = "get_leave_count", description = "연차 갯수 조회", prompt = "현재 사용 가능한 남은 연차 일수를 알려줘.", mappingId = "HR_VAC_02")
@McpFunction(register = false, displayName = "get_leave_count 툴", name = "get_leave_count", description = "연차 갯수 조회", prompt = "현재 사용 가능한 남은 연차 일수를 알려줘.", mappingId = "HR_VAC_02")
public Object getLeaveCount(LeaveCountReq data) {
return executeLegacy("HTTP", "HR_VAC_02", data);
}
@McpFunction(displayName = "secret_tool 툴", name = "secret_tool", description = "비공개 툴 테스트", prompt = "숨겨진 툴 강제 호출", mappingId = "SECRET_001", visible = false)
@McpFunction(register = false, displayName = "secret_tool 툴", name = "secret_tool", description = "비공개 툴 테스트", prompt = "숨겨진 툴 강제 호출", mappingId = "SECRET_001", visible = false)
public Object secretTool(LeaveCountReq data) {
return executeLegacy("HTTP", "SECRET_001", data);
}

View File

@@ -25,12 +25,12 @@ import io.shinhanlife.dap.mcc.dto.ContractDetailReq;
*/
public class ContractInquiryService extends AbstractMcpToolService {
@McpFunction(displayName = "contract_status 툴", name = "contract_status", description = "계약상태 조회", prompt = "김신한 고객의 현재 계약 상태를 조회해줘.", mappingId = "CNTR_001")
@McpFunction(register = false, displayName = "contract_status 툴", name = "contract_status", description = "계약상태 조회", prompt = "김신한 고객의 현재 계약 상태를 조회해줘.", mappingId = "CNTR_001")
public Object getStatus(ContractStatusReq data) {
return executeLegacy("HTTP", "CNTR_001", data);
}
@McpFunction(displayName = "contract_detail 툴", name = "contract_detail", description = "계약상세 조회", prompt = "김신한 고객의 계약 상세 내역을 알려줘.", mappingId = "CNTR_002")
@McpFunction(register = false, displayName = "contract_detail 툴", name = "contract_detail", description = "계약상세 조회", prompt = "김신한 고객의 계약 상세 내역을 알려줘.", mappingId = "CNTR_002")
public Object getDetail(ContractDetailReq data) {
return executeLegacy("HTTP", "CNTR_002", data);
}

View File

@@ -25,12 +25,12 @@ import io.shinhanlife.dap.mcc.dto.CustomerDetailReq;
*/
public class CustomerInfoService extends AbstractMcpToolService {
@McpFunction(displayName = "grade 툴", name = "grade", description = "고객등급 조회", prompt = "이 고객의 VIP 등급을 조회해줘.", mappingId = "CRM_001")
@McpFunction(register = false, displayName = "grade 툴", name = "grade", description = "고객등급 조회", prompt = "이 고객의 VIP 등급을 조회해줘.", mappingId = "CRM_001")
public Object getGrade(CustomerGradeReq req) {
return executeLegacy("TCP", "CRM_001", req);
}
@McpFunction(displayName = "detail 툴", name = "detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002")
@McpFunction(register = false, displayName = "detail 툴", name = "detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002")
public Object getDetail(CustomerDetailReq data) {
return executeLegacy("TCP", "CRM_002", data);
}

View File

@@ -0,0 +1,54 @@
package io.shinhanlife.dap.mcc.service;
import io.shinhanlife.dap.mcc.annotation.McpFunction;
import io.shinhanlife.dap.mcc.annotation.McpTool;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Random;
/**
* @package io.shinhanlife.dap.mcc.service
* @className DailyQuoteToolService
* @description 랜덤 명언 제공 툴
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
@Service
@McpTool(
routingType = "DIRECT",
categoryKey = "sample"
)
public class DailyQuoteToolService extends AbstractMcpToolService {
public record DailyQuoteReq(String category) {}
public record DailyQuoteRes(String quote, String author) {}
private final List<DailyQuoteRes> quotes = List.of(
new DailyQuoteRes("성공은 매일 반복한 작은 노력들의 합이다.", "로버트 콜리어"),
new DailyQuoteRes("시작이 반이다.", "아리스토텔레스"),
new DailyQuoteRes("포기하지 않는 한 실패는 없다.", "알베르트 아인슈타인"),
new DailyQuoteRes("가장 큰 위험은 위험 없는 삶이다.", "스티븐 코비")
);
@McpFunction(register = true, displayName = "랜덤 명언 툴", name = "daily_quote",
description = "무작위로 영감을 주는 명언을 하나 가져옵니다.",
prompt = "오늘의 명언 하나 알려줘, 동기부여 명언 등",
mappingId = "QUOTE_001"
)
public DailyQuoteRes execute(DailyQuoteReq req) {
int index = new Random().nextInt(quotes.size());
DailyQuoteRes selected = quotes.get(index);
log.info("[DailyQuoteTool] 명언 제공 완료: {}", selected.author());
return selected;
}
}

View File

@@ -0,0 +1,59 @@
package io.shinhanlife.dap.mcc.service;
import io.shinhanlife.dap.mcc.annotation.McpFunction;
import io.shinhanlife.dap.mcc.annotation.McpTool;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import lombok.extern.slf4j.Slf4j;
/**
* @package io.shinhanlife.dap.mcc.service
* @className ExchangeRateToolService
* @description 실시간 환율 조회 툴
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
@Service
@McpTool(
routingType = "DIRECT",
categoryKey = "sample"
)
public class ExchangeRateToolService extends AbstractMcpToolService {
public record ExchangeRateReq(String currencyCode) {}
public record ExchangeRateRes(String baseCurrency, String targetCurrency, double rate) {}
private final RestClient restClient;
public ExchangeRateToolService() {
this.restClient = RestClient.create();
}
@McpFunction(register = true, displayName = "실시간 환율 조회 툴", name = "exchange_rate",
description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)",
prompt = "현재 달러 환율 알려줘, 엔화 환율은?",
mappingId = "EXCHANGE_001"
)
public ExchangeRateRes execute(ExchangeRateReq req) {
String targetCurrency = req.currencyCode() != null ? req.currencyCode().toUpperCase().trim() : "USD";
// 간단한 모의 데이터로 반환 (실제 구현 시 외부 연동)
double dummyRate = 1350.50;
if (targetCurrency.contains("JPY")) {
dummyRate = 905.20;
} else if (targetCurrency.contains("EUR")) {
dummyRate = 1450.30;
}
log.info("[ExchangeRateTool] 환율 조회 완료: {} -> {}", targetCurrency, dummyRate);
return new ExchangeRateRes("KRW", targetCurrency, dummyRate);
}
}

View File

@@ -0,0 +1,71 @@
package io.shinhanlife.dap.mcc.service;
import io.shinhanlife.dap.common.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
import org.springframework.stereotype.Service;
import io.shinhanlife.dap.mcc.annotation.McpFunction;
import io.shinhanlife.dap.mcc.annotation.McpTool;
import io.shinhanlife.dap.mcc.dto.Onnba3011ReqDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @package io.shinhanlife.dap.mcc.service
* @className OnnbaMciToolService
* @description 보종By가입설계한도계산조회 MCI 연동 툴
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
@McpTool(routingType = "MCI", categoryKey = "other")
public class OnnbaMciToolService {
private static final String INTERFACE_CODE_3011 = "CLCNNB00001";
// Glow 기반의 AxhubMciComponent 주입
private final AxhubMciComponent mci;
/**
* AI Agent가 호출하게 될 메서드입니다.
* @McpFunction 어노테이션 하나로 AI 도구로 자동 노출 및 라우팅됩니다.
*/
@McpFunction(
register = true,
name = "calculate_subscription_limit",
displayName = "보종By가입설계한도계산조회",
description = "MCI 연동을 통해 보종By가입설계한도계산조회를 수행합니다.",
prompt = "가입설계 한도를 계산하고 조회해줘.",
mappingId = INTERFACE_CODE_3011
)
public Object callOnnba3011(Onnba3011ReqDto req) {
log.info("[MCI Tool] 보종By가입설계한도계산조회 요청 수신.");
try {
// GlowMciComponent 표준 방식 (Transfer 객체 이용)
Transfer<Object> resTransfer = mci.callTo(
INTERFACE_CODE_3011,
null, // rcvSvcId
req,
Object.class
);
log.info("[MCI Tool] Glow 기반 MCI 연동 성공.");
// 결과 반환 (실제로는 resTransfer.getBody() 리턴)
return resTransfer.getBody() != null ? resTransfer.getBody() : "{\"status\":\"SUCCESS\", \"message\":\"GlowMciComponent 통신 완료\"}";
} catch (Exception e) {
log.error("[MCI Tool] MCI 연동 중 오류 발생: {}", e.getMessage(), e);
return "{\"status\":\"ERROR\", \"message\":\"MCI 통신 실패: " + e.getMessage() + "\"}";
}
}
}

View File

@@ -0,0 +1,84 @@
package io.shinhanlife.dap.mcc.service;
import io.shinhanlife.dap.common.adapter.sender.ShinhanMciSender;
import io.shinhanlife.dap.common.integration.mci.dto.MciRequestWrapper;
import io.shinhanlife.dap.common.integration.mci.dto.ShinhanCommonHeaderDto;
import io.shinhanlife.dap.mcc.annotation.McpFunction;
import io.shinhanlife.dap.mcc.annotation.McpTool;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
/**
* @package io.shinhanlife.dap.mcc.service
* @className SampleMciToolService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Slf4j
@RequiredArgsConstructor
@McpTool(routingType = "MCI", categoryKey = "other") // MCP Tool 등록 어노테이션
public class SampleMciToolService {
// EIMS/MCI 발송을 담당하는 Sender 주입
private final ShinhanMciSender shinhanMciSender;
@Value("${shinhan.integration.mci.default-url:http://localhost:8081/api/mock/esb/api}")
private String mciTargetUrl;
// AI가 인식할 파라미터 DTO
@Data
public static class SampleMciReqDto {
private String customerId;
private String inquiryType;
}
/**
* AI Agent가 호출하게 될 메서드입니다.
*/
@McpFunction(register = false, name = "inquiry_customer_mci",
displayName = "고객 정보 조회 (MCI)",
description = "MCI 연동을 통해 고객의 상세 정보를 조회합니다.",
prompt = "고객 정보를 조회해줘.",
mappingId = "CUST_INQ_001"
)
public Object inquiryCustomerInfo(SampleMciReqDto req) {
log.info("[MCI Tool] 고객 정보 조회 요청 수신. 고객ID: {}", req.getCustomerId());
try {
// 1. MCI 전문 통신을 위한 Wrapper 객체 생성
MciRequestWrapper<SampleMciReqDto> wrapper = new MciRequestWrapper<>();
// 2. 공통 헤더 세팅 (인터페이스 ID, 서비스 ID 등 업무에 맞게 설정)
ShinhanCommonHeaderDto header = new ShinhanCommonHeaderDto();
header.setItrIfId("CUST_INQ_001");
header.setRcvSvcId("INQ9040");
wrapper.setTgrmCmnnhddValu(header);
// 3. 비즈니스 데이터(Body) 세팅
wrapper.setBody(req);
// 4. ShinhanMciSender를 통해 실제 연동 수행 및 응답 수신
log.info("[MCI Tool] ShinhanMciSender 연동 시작... (URL: {})", mciTargetUrl);
String responseJson = shinhanMciSender.send(mciTargetUrl, wrapper);
log.info("[MCI Tool] MCI 연동 성공. 응답 수신 완료");
// 결과 반환 (이 문자열은 JSON 파싱되거나 그대로 AI에게 전달됩니다)
return responseJson;
} catch (Exception e) {
log.error("[MCI Tool] MCI 연동 중 오류 발생: {}", e.getMessage(), e);
return "{\"status\":\"ERROR\", \"message\":\"MCI 통신 실패: " + e.getMessage() + "\"}";
}
}
}

Some files were not shown because too many files have changed in this diff Show More