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

View File

@@ -45,6 +45,7 @@ DAP Backend는 2개의 주요 애플리케이션으로 분리 운영됩니다:
- `HR`: 휴가 등록, 연차 갯수 조회 - `HR`: 휴가 등록, 연차 갯수 조회
- `CONTRACT`: 계약 상태, 계약 상세 조회 - `CONTRACT`: 계약 상태, 계약 상세 조회
- `CUSTOMER`: 고객 등급, 고객 상세 정보 조회 - `CUSTOMER`: 고객 등급, 고객 상세 정보 조회
- `SAMPLE`: 날씨, 환율, 명언 조회 등 외부 연동 샘플
- IntelliJ IDEA: `Run/Debug Configurations`에서 `DapTool*Application``Program arguments``--mcp.tool.target=NOTIFICATION` 입력 - 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 등) 연동 ### 2. 프로덕션 클라우드 AI (Google Cloud Agent Builder 등) 연동
실제 라이브 서비스에서 동작하는 클라우드 Agent Builder는 REST API 기반의 OpenAPI Spec을 요구합니다. 실제 라이브 서비스에서 동작하는 클라우드 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 gradlew .
COPY gradle gradle COPY gradle gradle
COPY build.gradle settings.gradle ./ COPY build.gradle settings.gradle ./
COPY dap-common dap-common
COPY dap-tool-core dap-tool-core
COPY dap-gateway dap-gateway COPY dap-gateway dap-gateway
RUN chmod +x gradlew RUN chmod +x gradlew
RUN ./gradlew :dap-gateway:build -x test RUN ./gradlew :dap-gateway:build -x test

View File

@@ -3,7 +3,7 @@ plugins {
} }
dependencies { 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-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-data-redis'

View File

@@ -162,22 +162,53 @@ public class ExecuteService {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
String responseText = ""; 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, ""); auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText); redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
return result; return finalResult;
} catch (ToolExecutionException error) { } catch (ToolExecutionException error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name()); auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
redisTrace.finished(context, metadata, argumentsNode, 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) { } catch (Exception error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name()); auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
redisTrace.finished(context, metadata, argumentsNode, 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()) { if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
targetUrl = metadata.getPodUrl(); 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); ObjectNode pageArguments = paginationValidator.normalize(arguments);
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId()); LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
while (true) { while (true) {
Map<String, Object> pagePayload = new HashMap<>(payload); Map<String, Object> pagePayload = objectMapper.convertValue(pageArguments, Map.class);
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));
}
try { try {
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload)))); log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload))));
@@ -251,13 +279,16 @@ public class ExecuteService {
JsonNode data = null; JsonNode data = null;
try { 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) { } catch (Exception e) {
if (executeApiUrl.contains("http://tool-")) { if (executeApiUrl.contains("http://tool-")) {
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost"); String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl); log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
try { try {
data = toolInvoker.invoke(pagePayload, fallbackUrl); data = toolInvoker.invoke(pagePayload, fallbackUrl, headers);
} catch (Exception ex) { } catch (Exception ex) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage()); throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
} }

View File

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

View File

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

View File

@@ -22,5 +22,5 @@ import java.util.Map;
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다. * Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
*/ */
public interface ToolInvoker { 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.url=jdbc:oracle:thin:@//dev-db-host:1521/XEPDB1
# spring.datasource.driverClassName=oracle.jdbc.OracleDriver # spring.datasource.driverClassName=oracle.jdbc.OracleDriver
# spring.datasource.username=dev_user # spring.datasource.username=dev_user
# spring.datasource.password=dev_password # 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.sms=https://dap-tool-sms.onrender.com
mcp.gateway.fallback.routes.email=https://dap-tool-email.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.default-url=https://dap-tool-other.onrender.com
mcp.gateway.fallback.routes.payment=https://dap-tool-payment.onrender.com mcp.gateway.fallback.routes.payment=https://dap-tool-payment.onrender.com
mcp.gateway.fallback.routes.hr=https://dap-tool-hr.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.envrTypeCd=D
shinhan.integration.eai.url=http://10.176.32.181 shinhan.integration.eai.url=http://10.176.32.181
shinhan.integration.internalMci.url=http://10.176.32.173 shinhan.integration.internalMci.url=http://10.176.32.173

View File

@@ -464,6 +464,7 @@
<label class="form-label">Domain Category</label> <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> <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"> <datalist id="groupList">
<option value="sample">sample</option>
<option value="common">common</option> <option value="common">common</option>
<option value="customer">customer</option> <option value="customer">customer</option>
<option value="contract">contract</option> <option value="contract">contract</option>

View File

@@ -3,7 +3,30 @@ plugins {
} }
dependencies { 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 { 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> * </pre>
*/ */
@SpringBootApplication(scanBasePackages = {"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"}) @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 @EnableCaching
public class DapTool%sApplication { public class DapTool%sApplication {
public static void main(String[] args) { public static void main(String[] args) {
@@ -135,12 +135,12 @@ public class PodScaffolder {
# Suppress Kafka Connection Logs # Suppress Kafka Connection Logs
logging.level.org.apache.kafka=ERROR logging.level.org.apache.kafka=ERROR
# Auto Prefix Namespace # Auto Prefix Namespace (Disabled by default)
mcp.namespace=%s mcp.namespace=
# API 보안 설정 # API 보안 설정
mcp.security.tenant-domains.TESTER-DEV=ALL mcp.security.tenant-domains.TESTER-DEV=ALL
""".formatted(portStr, moduleName, shortName); """.formatted(portStr, moduleName);
Files.writeString(resPath.resolve("application.properties"), applicationProperties); Files.writeString(resPath.resolve("application.properties"), applicationProperties);
String applicationLocalProperties = """ String applicationLocalProperties = """
@@ -163,7 +163,7 @@ public class PodScaffolder {
# Gateway/Tool URLs # Gateway/Tool URLs
axhub.gateway.url=http://localhost:8081 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 # Disable Kafka
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration 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 baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: ");
String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): "); String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): ");
String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): "); 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"; if (group.isEmpty()) group = "COMMON";
String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): "); String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): ");
if (routingType.trim().isEmpty()) { if (routingType.trim().isEmpty()) {

View File

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

View File

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

View File

@@ -120,10 +120,7 @@ public class ToolRegistryHeartbeatSender {
if (method.getParameterCount() > 0) { if (method.getParameterCount() > 0) {
try { try {
Class<?> paramType = method.getParameterTypes()[0]; Class<?> paramType = method.getParameterTypes()[0];
Map<String, Object> schema = JsonSchemaGenerator.generatePropertiesSchema(paramType); Map<String, Object> finalSchema = JsonSchemaGenerator.generateSchema(paramType);
Map<String, Object> finalSchema = new HashMap<>();
finalSchema.put("type", "object");
finalSchema.put("properties", schema);
meta.setParametersSchema(finalSchema); meta.setParametersSchema(finalSchema);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to generate schema for {}", subToolName, 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;
import com.fasterxml.jackson.annotation.JsonProperty;
/** import com.fasterxml.jackson.annotation.JsonPropertyDescription;
* @package io.shinhanlife.dap.mcc.util import io.shinhanlife.dap.mcc.annotation.McpParameter;
* @className JsonSchemaGenerator import java.lang.reflect.Field;
* @description AX HUB 시스템 처리 클래스 import java.util.ArrayList;
* @author 김형식 import java.util.HashMap;
* @create 2026.09.01 import java.util.List;
* <pre> import java.util.Map;
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용 public class JsonSchemaGenerator {
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 /**
* * Java DTO 클래스를 분석하여 MCP 규격의 완전한 JSON Schema를 생성합니다.
* </pre> */
*/ public static Map<String, Object> generateSchema(Class<?> clazz) {
import io.shinhanlife.dap.mcc.annotation.McpParameter; Map<String, Object> schema = new HashMap<>();
import java.lang.reflect.Field; schema.put("type", "object");
import java.util.HashMap;
import java.util.List; Map<String, Object> properties = new HashMap<>();
import java.util.Map; List<String> requiredList = new ArrayList<>();
public class JsonSchemaGenerator { for (Field field : clazz.getDeclaredFields()) {
Map<String, Object> fieldSchema = new HashMap<>();
/**
* Java DTO 클래스를 분석하여 MCP 규격의 JSON Schema (Properties)를 생성합니다. // 1. 타입 매핑
*/ fieldSchema.put("type", mapJavaTypeToJsonType(field.getType()));
public static Map<String, Object> generatePropertiesSchema(Class<?> clazz) {
Map<String, Object> properties = new HashMap<>(); // 2. 어노테이션 기반 설명 추출
McpParameter paramAnnotation = field.getAnnotation(McpParameter.class);
for (Field field : clazz.getDeclaredFields()) { JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class);
Map<String, Object> fieldSchema = new HashMap<>(); if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
fieldSchema.put("description", paramAnnotation.description());
// 1. 타입 매핑 } else if (descAnnotation != null && !descAnnotation.value().isEmpty()) {
fieldSchema.put("type", mapJavaTypeToJsonType(field.getType())); fieldSchema.put("description", descAnnotation.value());
} else {
// 2. 어노테이션 기반 설명 추출 fieldSchema.put("description", field.getName()); // 기본값
McpParameter paramAnnotation = field.getAnnotation(McpParameter.class); }
if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
fieldSchema.put("description", paramAnnotation.description()); // 3. 필수 여부 판단
} else { JsonProperty jsonProp = field.getAnnotation(JsonProperty.class);
fieldSchema.put("description", field.getName()); // 기본값 if ((jsonProp != null && jsonProp.required()) || (paramAnnotation != null && paramAnnotation.required())) {
} requiredList.add(field.getName());
}
properties.put(field.getName(), fieldSchema);
} properties.put(field.getName(), fieldSchema);
}
return properties;
} schema.put("properties", properties);
if (!requiredList.isEmpty()) {
private static String mapJavaTypeToJsonType(Class<?> clazz) { schema.put("required", requiredList);
if (clazz == String.class) return "string"; }
if (clazz == Integer.class || clazz == int.class) return "integer";
if (clazz == Long.class || clazz == long.class) return "integer"; return schema;
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"; private static String mapJavaTypeToJsonType(Class<?> clazz) {
if (List.class.isAssignableFrom(clazz)) return "array"; if (clazz == String.class) return "string";
return "object"; 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 gradlew .
COPY gradle gradle COPY gradle gradle
COPY build.gradle settings.gradle ./ COPY build.gradle settings.gradle ./
COPY dap-common dap-common
COPY dap-tool-core dap-tool-core COPY dap-tool-core dap-tool-core
COPY dap-tool-email dap-tool-email COPY dap-tool-email dap-tool-email
RUN chmod +x gradlew 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.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching; 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"}) @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"}) @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 @EnableCaching
public class DapToolEmailApplication { public class DapToolEmailApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -26,7 +26,7 @@ import java.util.Map;
@McpTool(routingType = "EAI", categoryKey = "notification") @McpTool(routingType = "EAI", categoryKey = "notification")
public class EmailToolService extends AbstractMcpToolService { 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) { public Object sendEmail(EmailSendReq req) {
log.info("[Email] 이메일 발송 요청 수신. 수신자: {}", req.getEmailAddress()); log.info("[Email] 이메일 발송 요청 수신. 수신자: {}", req.getEmailAddress());

View File

@@ -1,9 +1,9 @@
# Render 클라우드 환경 전용 설정 # Render ?渠𦉘?圉<> ?瞘祭 ?<3F>鹻 ?木<>
server.port=${PORT:8083} 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 axhub.tool.url=https://dap-tool-email.onrender.com
# EIMS 동적 라우팅 접속 정보 (Mock) # EIMS ?軤<> ?潰黱???𡢾<> ?瑅陷 (Mock)
eims.http.url=http://localhost:${server.port}/api/gateway eims.http.url=http://localhost:${server.port}/api/gateway
eims.tcp.host=127.0.0.1 eims.tcp.host=127.0.0.1
eims.tcp.port=8090 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.mci.url=http://localhost:${server.port}/api/mock/esb/api
eims.mcistring.url=http://localhost:${server.port}/api/mock/esb/string eims.mcistring.url=http://localhost:${server.port}/api/mock/esb/string
# --- 신한라이프 EAI/MCI 연계 IP 정보 (개발 환경) --- # --- ?𡥄<>?潰𦚯??EAI/MCI ?國<> IP ?瑅陷 (穈嶅<E7A988> ?瞘祭) ---
shinhan.integration.envrTypeCd=D shinhan.integration.envrTypeCd=D
shinhan.integration.eai.url=http://10.176.32.181 shinhan.integration.eai.url=http://10.176.32.181
shinhan.integration.internalMci.url=http://10.176.32.173 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 # Gateway/Tool URLs
axhub.gateway.url=http://localhost:8081 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 # Disable Kafka
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration 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 logging.level.org.apache.kafka=ERROR
# Auto Prefix Namespace # Auto Prefix Namespace
mcp.namespace=email mcp.namespace=
# API 보안 키 설정 # API 보안 키 설정
mcp.security.tenant-domains.TESTER-DEV=ALL mcp.security.tenant-domains.TESTER-DEV=ALL

View File

@@ -3,7 +3,7 @@ WORKDIR /app
COPY gradlew . COPY gradlew .
COPY gradle gradle COPY gradle gradle
COPY build.gradle settings.gradle ./ COPY build.gradle settings.gradle ./
COPY dap-common dap-common
COPY dap-tool-core dap-tool-core COPY dap-tool-core dap-tool-core
COPY dap-tool-other dap-tool-other COPY dap-tool-other dap-tool-other
RUN chmod +x gradlew 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.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching; 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"}) @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"}) @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 @EnableCaching
public class DapToolOtherApplication { public class DapToolOtherApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

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

View File

@@ -30,12 +30,12 @@ import io.shinhanlife.dap.mcc.dto.BillingProcessReq;
@lombok.extern.slf4j.Slf4j @lombok.extern.slf4j.Slf4j
public class BillingProcessService extends AbstractMcpToolService { 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) { public Object getStatus(BillingStatusReq data) {
return executeBillingLogic("BILL_001", 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) { public Object processBilling(BillingProcessReq data) {
return executeBillingLogic("BILL_002", data); return executeBillingLogic("BILL_002", data);
} }

View File

@@ -25,12 +25,12 @@ import io.shinhanlife.dap.mcc.dto.BondIssueReq;
*/ */
public class BondIssueService extends AbstractMcpToolService { 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) { public Object check(BondCheckReq data) {
return executeLegacy("EAI", "BOND_001", 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) { public Object issue(BondIssueReq data) {
return executeLegacy("EAI", "BOND_002", 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 { 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) { public Object registerVacation(VacationRegisterReq data) {
return executeLegacy("HTTP", "HR_VAC_01", 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) { public Object getLeaveCount(LeaveCountReq data) {
return executeLegacy("HTTP", "HR_VAC_02", 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) { public Object secretTool(LeaveCountReq data) {
return executeLegacy("HTTP", "SECRET_001", 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 { 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) { public Object getStatus(ContractStatusReq data) {
return executeLegacy("HTTP", "CNTR_001", 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) { public Object getDetail(ContractDetailReq data) {
return executeLegacy("HTTP", "CNTR_002", 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 { 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) { public Object getGrade(CustomerGradeReq req) {
return executeLegacy("TCP", "CRM_001", 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) { public Object getDetail(CustomerDetailReq data) {
return executeLegacy("TCP", "CRM_002", 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