Compare commits

...

64 Commits

Author SHA1 Message Date
jade
6968c7dfb7 refactor: use Spring DI for MapStruct and update AGENTS.md rule 2026-07-22 12:37:16 +09:00
jade
416b068fcb 소스 삭제 2026-07-22 11:36:29 +09:00
jade
933ba2ecfa 소스 수정 2026-07-22 11:35:56 +09:00
jade
ce96137948 fix: resolve Format specifier '%s' error in ToolScaffolder and generate SearchHr tool 2026-07-22 11:31:39 +09:00
jade
e983b47af6 feat: add MapStruct mapping samples to ToolScaffolder template 2026-07-22 11:22:36 +09:00
jade
1796370a35 docs: add Shinhan Life logging standard rules to AGENTS.md 2026-07-22 10:40:16 +09:00
jade
680dbe098b feat: apply Shinhan Life logging standards to all modules 2026-07-22 10:22:16 +09:00
jade
a877103d93 fix: add missing java.util.Map import and remove fully qualified names in ToolScaffolder template 2026-07-22 09:54:57 +09:00
jade
6e1ef40380 fix: use Map instead of JSON string for fallback returns in ToolScaffolder and SearchHrService to resolve serialization and IDE warnings 2026-07-22 09:51:27 +09:00
jade
a59249e1db fix: always generate legacy DTOs and converter regardless of routing type 2026-07-22 09:24:07 +09:00
jade
128fc3d022 feat: update ToolScaffolder to generate legacy DTOs in 'legacy' and converters in 'converter' package 2026-07-22 09:22:50 +09:00
jade
899053aef5 소스 경로 변경 2026-07-22 09:21:05 +09:00
jade
0a5bc52e6a 소스 경로 변경 2026-07-22 09:14:14 +09:00
jade
885963c81c 소스 경로 변경 2026-07-21 22:33:35 +09:00
jade
b8a8da3b3c 소스 경로 변경 2026-07-21 22:29:06 +09:00
jade
1a30c3a0cd 필요 없는 소스 삭제 2026-07-21 22:19:09 +09:00
jade
50b6b8b9b0 remove: Delete MciSampleTool and related DTOs 2026-07-21 22:18:10 +09:00
jade
140be4c7e9 fix: Scaffold dropdown missing modules on Render 2026-07-21 22:11:21 +09:00
jade
75b93d47c2 fix: Restore Korean character encoding in application-dev.properties 2026-07-21 21:57:34 +09:00
jade
f880f391d5 fix: Update fallback URLs from dap-tool to axhub-tool 2026-07-21 21:55:40 +09:00
jade
8a53bff74f Revert "refactor: Remove fallback HTTP logic from McpRouterController"
This reverts commit a46943ca23.
2026-07-21 21:53:36 +09:00
jade
a46943ca23 refactor: Remove fallback HTTP logic from McpRouterController 2026-07-21 21:49:32 +09:00
jade
11b8b08890 redis 연동 테스트 2026-07-21 21:30:28 +09:00
jade
2138ab7efa feat: Add MCI Scaffolding support and fix bean collision 2026-07-21 21:22:38 +09:00
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
137 changed files with 1768 additions and 1008 deletions

View File

@@ -5,7 +5,7 @@
## 개발 가이드라인 ## 개발 가이드라인
* 패키지명은 `controller` 대신 `presentation`을 사용합니다. * 패키지명은 `controller` 대신 `presentation`을 사용합니다.
* MapStruct 사용 시, 테스트 환경 에러를 방지하기 위해 생성자(`new ...Impl()`) 대신 `Mappers.getMapper(인터페이스명.class)` 방식으로 인스턴스를 가져옵니다. * MapStruct 사용 시, Spring DI를 활용하여 의존성 주입(`private final Converter converter;`)을 받는 방식을 권장합니다. (단위 테스트 시에는 `@MockBean` 또는 직접 구현체를 주입하여 테스트)
## 명심해야 할 규칙 추가란 ## 명심해야 할 규칙 추가란
* 이모지는 무조건 넣지 않는다 * 이모지는 무조건 넣지 않는다
@@ -30,3 +30,9 @@
* 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외) * 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외)
* 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행) * 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행)
* 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다. * 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다.
* 신한라이프 표준 로그 기준 (Logback 설정):
* **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dap-gateway/A01/`)
* **로그 네이밍 규칙:** `${HOSTNAME}_코드명_yyyyMMdd.log` (예: `${HOSTNAME}_A01_20260722.log`)
* **기본 로그 구분 코드:** 시스템 운영기록 가동기록의 경우 `A01`을 기본으로 사용한다.
* **호스트명 동적 할당:** Logback 설정 시 `<property name="HOSTNAME" value="${HOSTNAME}" />` 를 선언하여 사용한다.

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,125 +0,0 @@
import os
import re
JAVADOC_FILES = [
r'.\McpBridge.java',
r'.\dap-common\src\main\java\io\shinhanlife\dap\common\mcp\exception\GlobalExceptionHandler.java',
r'.\dap-common\src\main\java\io\shinhanlife\dap\common\util\ToolSourceUpdater.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowAppServiceId.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowControllerId.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowIndexPaging.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowMciFieldInfo.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowMybatisMapper.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowServiceGroupId.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowTrgmField.java',
r'.\dap-common\src\test\java\io\shinhanlife\glow\util\GlowMciParserTest.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\DapGatewayApplication.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\audit\AuditLogService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\config\AgentResponseBudgetProperties.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\config\McpGatewayProperties.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\guardrail\GuardrailService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\guardrail\SensitiveDataMasker.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\guardrail\ToolResponseGuardrailService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\presentation\ChatController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\presentation\McpRouterController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\presentation\ScaffoldingController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\redis\McpMonitorEventService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\redis\RedisToolTraceService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreaker.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreakerMonitorController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreakerService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreakerSnapshot.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreakerState.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\FailureType.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\RetryPolicy.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\ToolExecutionException.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\security\McpRequestContext.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\security\McpRequestContextResolver.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\security\ToolAuthorizationService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\service\ExecuteService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\sync\DynamicMcpController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\sync\DynamicMcpServerManager.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\sync\RegistryMcpToolSpecificationFactory.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\large\AgentResponseBudgetService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\large\LargeToolResponseService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\large\PaginationRequestValidator.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\result\ToolExecutionResult.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\result\ToolExecutionResultFormatter.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\transport\HttpToolInvoker.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\transport\ToolInvoker.java',
r'.\dap-gateway\src\test\java\io\shinhanlife\dap\biz\mcp\gateway\DapGatewayApplicationTests.java',
r'.\dap-gateway\src\test\java\io\shinhanlife\dap\biz\mcp\gateway\tool\large\PaginationRequestValidatorTest.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\sample\converter\SampleLegacyConverter.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\sample\dto\SampleAiReqDto.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\annotation\McpFunction.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\annotation\McpParameter.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\annotation\McpTool.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\aop\ToolSlaMonitoringAspect.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\presentation\BusinessToolController.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\service\ToolRegistryHeartbeatSender.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\util\JsonSchemaGenerator.java',
r'.\dap-tool-core\src\test\java\io\shinhanlife\dap\biz\mcp\sample\converter\SampleLegacyConverterTest.java',
r'.\dap-tool-email\src\main\java\io\shinhanlife\dap\biz\mcp\tool\email\DapToolEmailApplication.java',
r'.\dap-tool-other\src\main\java\io\shinhanlife\dap\biz\mcp\tool\dto\BalanceReq.java',
r'.\dap-tool-other\src\main\java\io\shinhanlife\dap\biz\mcp\tool\dto\BalanceRes.java',
r'.\dap-tool-other\src\main\java\io\shinhanlife\dap\biz\mcp\tool\other\DapToolOtherApplication.java',
r'.\dap-tool-other\src\main\java\io\shinhanlife\dap\biz\mcp\tool\service\BalanceService.java',
r'.\dap-tool-payment\src\main\java\io\shinhanlife\dap\biz\mcp\tool\payment\DapToolPaymentApplication.java',
r'.\dap-tool-sms\src\main\java\io\shinhanlife\dap\biz\mcp\tool\sms\DapToolSmsApplication.java',
r'.\dap-tool-sms\src\main\java\io\shinhanlife\dap\biz\mcp\tool\sms\converter\SmsLegacyConverter.java',
r'.\dap-tool-sms\src\main\java\io\shinhanlife\dap\biz\mcp\tool\sms\dto\SmsLegacyReqDto.java',
]
JAVADOC_TEMPLATE = '''/**
* @package {package}
* @className {classname}
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/'''
def get_package_and_class(content, filename):
pkg_match = re.search(r'^package\s+([^\s;]+)', content, re.MULTILINE)
package = pkg_match.group(1) if pkg_match else 'io.shinhanlife'
classname = os.path.splitext(filename)[0]
return package, classname
def add_javadoc(file_path):
if not os.path.exists(file_path):
print(f'SKIP (not found): {file_path}')
return
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if '@className' in content or '@author' in content:
print(f'SKIP (already has javadoc): {file_path}')
return
filename = os.path.basename(file_path)
package, classname = get_package_and_class(content, filename)
javadoc = JAVADOC_TEMPLATE.format(package=package, classname=classname)
# Insert after package declaration
pkg_pattern = re.compile(r'^(package\s+[^\s;]+;\s*\n)', re.MULTILINE)
match = pkg_pattern.search(content)
if match:
insert_pos = match.end()
new_content = content[:insert_pos] + '\n' + javadoc + '\n' + content[insert_pos:]
else:
new_content = javadoc + '\n' + content
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f'OK: {file_path}')
for f in JAVADOC_FILES:
add_javadoc(f)
print('\nDone!')

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

@@ -1,372 +0,0 @@
package io.shinhanlife.dap.common.util;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Scanner;
/**
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스
*
* [실행 방법]
* 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐)
* - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다.
* - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다.
*
* 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반)
* - 컴파일: javac -encoding UTF-8 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/util/ToolScaffolder.java
* - 실행: java -cp dap-tool-core/src/main/java io.shinhanlife.dap.mcc.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
*/
/**
* @package io.shinhanlife.dap.mcc.util
* @className ToolScaffolder
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class ToolScaffolder {
private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc";
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc";
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("=========================================");
System.out.println(" MCP Tool Scaffolder (Java CLI) ");
System.out.println("=========================================\n");
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 등): ");
if (group.isEmpty()) group = "COMMON";
String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): ");
if (routingType.trim().isEmpty()) {
routingType = "HTTP";
}
String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-tool-other): ");
if (moduleName.trim().isEmpty()) {
moduleName = "dap-tool-other";
}
String defaultAuthor = System.getProperty("user.name");
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
if (author.trim().isEmpty()) author = defaultAuthor;
String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
if (createDate.trim().isEmpty()) createDate = defaultDate;
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true);
System.out.println(result);
}
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
if (args.length > index) {
return args[index];
}
System.out.print(prompt);
return scanner.nextLine().trim();
}
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register) throws IOException {
baseName = toPascalCase(baseName);
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
Path serviceDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "service"));
Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "dto"));
String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, shortName, "dto"));
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, shortName, "converter"));
Files.createDirectories(serviceDir);
Files.createDirectories(dtoDir);
Files.createDirectories(legacyDtoDir);
Files.createDirectories(converterDir);
StringBuilder log = new StringBuilder();
// Generate Req DTO
String reqContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package %s.dto
* @className %sReq
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sReq {
// TODO: Add request fields here
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Req.java"), reqContent);
// Generate Res DTO
String resContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package %s.dto
* @className %sRes
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sRes {
private String status;
private String message;
// TODO: Add response fields here
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Res.java"), resContent);
String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
// Generate Service
String serviceContent = """
package %s.service;
import %s.annotation.McpFunction;
import %s.annotation.McpTool;
import %s.dto.%sReq;
import %s.dto.%sRes;
import %s.%s.converter.%sLegacyConverter;
import org.springframework.stereotype.Service;
import org.mapstruct.factory.Mappers;
/**
* @package %s.service
* @className %sService
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Service
@McpTool(
routingType = "%s",
categoryKey = "%s"
)
public class %sService extends AbstractMcpToolService {
private final %sLegacyConverter converter = Mappers.getMapper(%sLegacyConverter.class);
@McpFunction(
displayName = "%s 툴",
name = "%s",
description = "%s",
prompt = "%s",
mappingId = "%s",
register = %s,
requiresApproval = false
)
public Object execute(%sReq req) {
// %sLegacyReq legacyReq = converter.toLegacyReq(req);
return executeLegacy("%s", "%s", req); // Or pass legacyReq
}
}
""".formatted(
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, shortName, baseName,
BASE_PACKAGE,
baseName,
author,
createDate,
createDate, author,
routingType, group.toLowerCase(),
baseName,
baseName, baseName,
baseName, toolName, description, description + " 해줘.", interfaceId, register,
baseName,
baseName,
routingType, interfaceId
);
Files.writeString(serviceDir.resolve(baseName + "Service.java"), serviceContent);
// Generate Legacy Req DTO
String legacyReqContent = """
package %s.%s.dto;
import lombok.Data;
/**
* @package %s.%s.dto
* @className %sLegacyReq
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyReq {
// TODO: Add legacy request fields here
}
""".formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyReq.java"), legacyReqContent);
// Generate Legacy Res DTO
String legacyResContent = """
package %s.%s.dto;
import lombok.Data;
/**
* @package %s.%s.dto
* @className %sLegacyRes
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyRes {
// TODO: Add legacy response fields here
}
""".formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRes.java"), legacyResContent);
// Generate Legacy Converter
String converterContent = """
package %s.%s.converter;
import %s.dto.%sReq;
import %s.dto.%sRes;
import %s.%s.dto.%sLegacyReq;
import %s.%s.dto.%sLegacyRes;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @package %s.%s.converter
* @className %sLegacyConverter
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public interface %sLegacyConverter {
// @Mapping(source = "sourceField", target = "targetField")
%sLegacyReq toLegacyReq(%sReq req);
%sRes toRes(%sLegacyRes legacyRes);
}
""".formatted(
BASE_PACKAGE, shortName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, shortName, baseName,
BASE_PACKAGE, shortName, baseName,
BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author,
baseName, baseName, baseName, baseName, baseName, baseName
);
Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete!\n");
log.append("=========================================\n");
log.append("[Service] ").append(serviceDir.resolve(baseName + "Service.java")).append("\n");
log.append("[Req DTO] ").append(dtoDir.resolve(baseName + "Req.java")).append("\n");
log.append("[Res DTO] ").append(dtoDir.resolve(baseName + "Res.java")).append("\n");
log.append("[Legacy Req DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyReq.java")).append("\n");
log.append("[Legacy Res DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRes.java")).append("\n");
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\n");
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
return log.toString();
}
private static String toPascalCase(String str) {
if (str == null || str.isEmpty()) {
return str;
}
StringBuilder result = new StringBuilder();
boolean capitalizeNext = true;
for (char c : str.toCharArray()) {
if (c == '_' || c == '-' || c == ' ') {
capitalizeNext = true;
} else if (capitalizeNext) {
result.append(Character.toUpperCase(c));
capitalizeNext = false;
} else {
result.append(c);
}
}
if (result.length() > 0) {
result.setCharAt(0, Character.toUpperCase(result.charAt(0)));
}
return result.toString();
}
}

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

@@ -95,11 +95,13 @@ public class ScaffoldingController {
File dir = new File(sourceDir); File dir = new File(sourceDir);
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-tool-core")); File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-tool-core"));
if (files == null) return List.of("dap-tool-other"); if (files == null || files.length == 0) {
return List.of("dap-tool-other", "dap-tool-email", "dap-tool-hr", "dap-tool-payment", "dap-tool-sms");
}
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList()); return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) { } catch (Exception e) {
return List.of("dap-tool-other"); return List.of("dap-tool-other", "dap-tool-email", "dap-tool-hr", "dap-tool-payment", "dap-tool-sms");
} }
} }
} }

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

@@ -5,11 +5,11 @@
# spring.datasource.password=dev_password # spring.datasource.password=dev_password
# Render 클라우드 환경 전용 Fallback 라우팅 # Render 클라우드 환경 전용 Fallback 라우팅
mcp.gateway.fallback.routes.sms=https://dap-tool-sms.onrender.com mcp.gateway.fallback.routes.sms=https://axhub-tool-sms.onrender.com
mcp.gateway.fallback.routes.email=https://dap-tool-email.onrender.com mcp.gateway.fallback.routes.email=https://axhub-tool-email.onrender.com
mcp.gateway.fallback.default-url=https://dap-tool-other.onrender.com mcp.gateway.fallback.default-url=https://axhub-tool-other.onrender.com
mcp.gateway.fallback.routes.payment=https://dap-tool-payment.onrender.com mcp.gateway.fallback.routes.payment=https://axhub-tool-payment.onrender.com
mcp.gateway.fallback.routes.hr=https://dap-tool-hr.onrender.com mcp.gateway.fallback.routes.hr=https://axhub-tool-hr.onrender.com
# --- 신한라이프 EAI/MCI 연계 IP 정보 (개발 환경) --- # --- 신한라이프 EAI/MCI 연계 IP 정보 (개발 환경) ---
shinhan.integration.envrTypeCd=D shinhan.integration.envrTypeCd=D

View File

@@ -12,11 +12,14 @@
</appender> </appender>
<!-- 3. 파일(File) 출력 설정 (서버 운영용) --> <!-- 3. 파일(File) 출력 설정 (서버 운영용) -->
<!-- Logback에서 시스템 Hostname을 가져오기 위한 설정 -->
<property name="HOSTNAME" value="${HOSTNAME}" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/dap-gateway.log</file> <!-- 현재 로그가 쌓이는 파일 --> <file>/swlog/dap-gateway/A01/${HOSTNAME}_A01.log</file> <!-- 현재 로그가 쌓이는 파일 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 --> <!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 -->
<fileNamePattern>logs/dap-gateway-%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>/swlog/dap-gateway/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.log</fileNamePattern>
<!-- 최대 30일치 로그만 보관하고 오래된 것은 자동 삭제 --> <!-- 최대 30일치 로그만 보관하고 오래된 것은 자동 삭제 -->
<maxHistory>30</maxHistory> <maxHistory>30</maxHistory>
</rollingPolicy> </rollingPolicy>

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>
@@ -485,7 +486,11 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Target Module</label> <label class="form-label">Target Module</label>
<select class="form-select" name="moduleName" id="targetModuleSelect" required> <select class="form-select" name="moduleName" id="targetModuleSelect" required>
<option value="axhub-tool-other">axhub-tool-other</option> <option value="dap-tool-other">dap-tool-other</option>
<option value="dap-tool-email">dap-tool-email</option>
<option value="dap-tool-hr">dap-tool-hr</option>
<option value="dap-tool-payment">dap-tool-payment</option>
<option value="dap-tool-sms">dap-tool-sms</option>
</select> </select>
<div class="input-hint">Destination Pod directory.</div> <div class="input-hint">Destination Pod directory.</div>
</div> </div>
@@ -653,7 +658,7 @@
.then(modules => { .then(modules => {
const select = document.getElementById('targetModuleSelect'); const select = document.getElementById('targetModuleSelect');
if (modules && modules.length > 0) { if (modules && modules.length > 0) {
select.innerHTML = modules.map(m => `<option value="${m}" ${m === 'axhub-tool-other' ? 'selected' : ''}>${m}</option>`).join(''); select.innerHTML = modules.map(m => `<option value="${m}" ${m === 'dap-tool-other' ? 'selected' : ''}>${m}</option>`).join('');
} }
}) })
.catch(err => console.error('Failed to load modules:', err)); .catch(err => console.error('Failed to load modules:', err));

View File

@@ -0,0 +1,34 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
class DynamicMcpControllerRouteTest {
@Test
void exposesOnlySupportedCategoryAwareMcpRoutes() {
Set<String> getPaths = Arrays.stream(DynamicMcpController.class.getDeclaredMethods())
.flatMap(method -> Arrays.stream(method.getAnnotationsByType(GetMapping.class)))
.flatMap(mapping -> Arrays.stream(mapping.value()))
.collect(Collectors.toSet());
Set<String> postPaths = Arrays.stream(DynamicMcpController.class.getDeclaredMethods())
.flatMap(method -> Arrays.stream(method.getAnnotationsByType(PostMapping.class)))
.flatMap(mapping -> Arrays.stream(mapping.value()))
.collect(Collectors.toSet());
assertTrue(getPaths.contains("/mcp/sse/{category}"));
assertTrue(postPaths.contains("/mcp/message/{category}"));
assertTrue(postPaths.contains("/mcp/custom/{category}"));
assertFalse(postPaths.contains("/mcp"));
assertFalse(postPaths.contains("/mcp/initialize"));
assertFalse(postPaths.contains("/mcp/sse/initialize"));
}
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.biz.mcp.gateway.sync;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import org.junit.jupiter.api.Test;
class RegistryMcpToolSpecificationFactoryTest {
@Test
void exposesMetadataBehaviorHintsInMcpToolSpecification() {
ToolMetadata metadata = ToolMetadata.builder()
.name("customer_lookup")
.description("Looks up customer information")
.readOnlyHint(true)
.destructiveHint(false)
.idempotentHint(true)
.openWorldHint(false)
.build();
RegistryMcpToolSpecificationFactory factory = new RegistryMcpToolSpecificationFactory(null, new ObjectMapper());
McpSchema.Tool tool = factory.create(metadata).tool();
assertTrue(tool.annotations().readOnlyHint());
assertFalse(tool.annotations().destructiveHint());
assertTrue(tool.annotations().idempotentHint());
assertFalse(tool.annotations().openWorldHint());
}
}

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

@@ -21,7 +21,6 @@ import org.springframework.context.annotation.Configuration;
*/ */
@Getter @Getter
@Setter @Setter
@Configuration
@ConfigurationProperties(prefix = "shinhan.integration") @ConfigurationProperties(prefix = "shinhan.integration")
public class ShinhanIntegrationProperties { public class ShinhanIntegrationProperties {

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

@@ -0,0 +1,480 @@
package io.shinhanlife.dap.common.util;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Scanner;
/**
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스
*
* [실행 방법]
* 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐)
* - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다.
* - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다.
*
* 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반)
* - 컴파일: javac -encoding UTF-8 dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/util/ToolScaffolder.java
* - 실행: java -cp dap-tool-core/src/main/java io.shinhanlife.dap.mcc.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
*/
/**
* @package io.shinhanlife.dap.mcc.util
* @className ToolScaffolder
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class ToolScaffolder {
private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc";
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc";
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("=========================================");
System.out.println(" MCP Tool Scaffolder (Java CLI) ");
System.out.println("=========================================\n");
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 소속 그룹 (예: 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()) {
routingType = "HTTP";
}
String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-tool-other): ");
if (moduleName.trim().isEmpty()) {
moduleName = "dap-tool-other";
}
String defaultAuthor = System.getProperty("user.name");
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
if (author.trim().isEmpty()) author = defaultAuthor;
String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
if (createDate.trim().isEmpty()) createDate = defaultDate;
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true);
System.out.println(result);
}
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
if (args.length > index) {
return args[index];
}
System.out.print(prompt);
return scanner.nextLine().trim();
}
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register) throws IOException {
baseName = toPascalCase(baseName);
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
Path serviceDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "service"));
Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "dto"));
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "legacy"));
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "converter"));
Files.createDirectories(serviceDir);
Files.createDirectories(dtoDir);
Files.createDirectories(legacyDtoDir);
Files.createDirectories(converterDir);
StringBuilder log = new StringBuilder();
// Generate Req DTO
String reqContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import %s.annotation.McpParameter;
import lombok.Data;
/**
* @package %s.dto
* @className %sReq
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sReq {
@McpParameter(description = "수신자 전화번호", required = true)
private String phoneNumber;
@McpParameter(description = "전송할 메시지 내용", required = true)
private String message;
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Req.java"), reqContent);
// Generate Res DTO
String resContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package %s.dto
* @className %sRes
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sRes {
private String status;
private String message;
// TODO: Add response fields here
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Res.java"), resContent);
String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
boolean isMci = "MCI".equalsIgnoreCase(routingType);
String serviceContent;
if (isMci) {
serviceContent = """
package %s.service;
import %s.annotation.McpFunction;
import %s.annotation.McpTool;
import %s.dto.%sReq;
import %s.dto.%sRes;
import io.shinhanlife.dap.common.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import %s.converter.%sLegacyConverter;
import %s.legacy.%sLegacyReq;
/**
* @package %s.service
* @className %sService
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
@McpTool(
routingType = "%s",
categoryKey = "%s"
)
public class %sService {
private final AxhubMciComponent mci;
private final %sLegacyConverter converter;
@McpFunction(
displayName = "%s 툴",
name = "%s",
description = "%s",
prompt = "%s",
mappingId = "%s",
register = %s,
requiresApproval = false,
openWorldHint = true
)
public Object execute(%sReq req) {
log.info("[MCI Tool] {} 요청 수신.", "%s");
try {
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
%sLegacyReq legacyReq = converter.toLegacyReq(req);
Transfer<Object> resTransfer = mci.callTo(
"%s",
null,
legacyReq,
Object.class
);
return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS");
} catch (Exception e) {
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
}
}
}
""".formatted(
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
author,
createDate,
createDate, author,
routingType, group.toLowerCase(),
baseName,
baseName,
baseName, toolName, description, description + " 해줘.", interfaceId, register,
baseName,
baseName,
baseName,
interfaceId
);
} else {
serviceContent = """
package %s.service;
import %s.annotation.McpFunction;
import %s.annotation.McpTool;
import %s.dto.%sReq;
import %s.dto.%sRes;
import %s.converter.%sLegacyConverter;
import org.springframework.stereotype.Service;
/**
* @package %s.service
* @className %sService
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Service
@McpTool(
routingType = "%s",
categoryKey = "%s"
)
@RequiredArgsConstructor
public class %sService extends AbstractMcpToolService {
private final %sLegacyConverter converter;
@McpFunction(
displayName = "%s 툴",
name = "%s",
description = "%s",
prompt = "%s",
mappingId = "%s",
register = %s,
requiresApproval = false
)
public Object execute(%sReq req) {
// %sLegacyReq legacyReq = converter.toLegacyReq(req);
return executeLegacy("%s", "%s", req); // Or pass legacyReq
}
}
""".formatted(
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
author,
createDate,
createDate, author,
routingType, group.toLowerCase(),
baseName,
baseName,
baseName, toolName, description, description + " 해줘.", interfaceId, register,
baseName,
baseName,
routingType, interfaceId
);
}
Files.writeString(serviceDir.resolve(baseName + "Service.java"), serviceContent);
String legacyReqContent = """
package %s.legacy;
import lombok.Data;
/**
* @package %s.legacy
* @className %sLegacyReq
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyReq {
/**
* EAI 시스템이 요구하는 수신자 번호 파라미터명
*/
private String phone;
/**
* EAI 시스템이 요구하는 메시지 내용 파라미터명
*/
private String content;
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyReq.java"), legacyReqContent);
String legacyResContent = """
package %s.legacy;
import lombok.Data;
/**
* @package %s.legacy
* @className %sLegacyRes
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyRes {
// TODO: Add legacy response fields here
}
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRes.java"), legacyResContent);
String converterContent = """
package %s.converter;
import %s.dto.%sReq;
import %s.dto.%sRes;
import %s.legacy.%sLegacyReq;
import %s.legacy.%sLegacyRes;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @package %s.converter
* @className %sLegacyConverter
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public interface %sLegacyConverter {
@Mapping(source = "phoneNumber", target = "phone")
@Mapping(source = "message", target = "content")
%sLegacyReq toLegacyReq(%sReq req);
@Mapping(source = "phone", target = "phoneNumber")
@Mapping(source = "content", target = "message")
%sReq toReq(%sLegacyReq legacyReq);
// %sRes toRes(%sLegacyRes legacyRes);
}
""".formatted(
BASE_PACKAGE,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName,
BASE_PACKAGE, baseName, author, createDate, createDate, author,
baseName, baseName, baseName, baseName, baseName, baseName, baseName
);
Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
log.append("=========================================\n");
log.append("[Service] ").append(serviceDir.resolve(baseName + "Service.java")).append("\n");
log.append("[Req DTO] ").append(dtoDir.resolve(baseName + "Req.java")).append("\n");
log.append("[Res DTO] ").append(dtoDir.resolve(baseName + "Res.java")).append("\n");
log.append("[Legacy Req DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyReq.java")).append("\n");
log.append("[Legacy Res DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRes.java")).append("\n");
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\n");
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
return log.toString();
}
private static String toPascalCase(String str) {
if (str == null || str.isEmpty()) {
return str;
}
StringBuilder result = new StringBuilder();
boolean capitalizeNext = true;
for (char c : str.toCharArray()) {
if (c == '_' || c == '-' || c == ' ') {
capitalizeNext = true;
} else if (capitalizeNext) {
result.append(Character.toUpperCase(c));
capitalizeNext = false;
} else {
result.append(c);
}
}
if (result.length() > 0) {
result.setCharAt(0, Character.toUpperCase(result.charAt(0)));
}
return result.toString();
}
}

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,22 +1,10 @@
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
* @className JsonSchemaGenerator
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcc.annotation.McpParameter; import io.shinhanlife.dap.mcc.annotation.McpParameter;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -24,10 +12,14 @@ import java.util.Map;
public class JsonSchemaGenerator { public class JsonSchemaGenerator {
/** /**
* Java DTO 클래스를 분석하여 MCP 규격의 JSON Schema (Properties)를 생성합니다. * Java DTO 클래스를 분석하여 MCP 규격의 완전한 JSON Schema를 생성합니다.
*/ */
public static Map<String, Object> generatePropertiesSchema(Class<?> clazz) { public static Map<String, Object> generateSchema(Class<?> clazz) {
Map<String, Object> schema = new HashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new HashMap<>(); Map<String, Object> properties = new HashMap<>();
List<String> requiredList = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) { for (Field field : clazz.getDeclaredFields()) {
Map<String, Object> fieldSchema = new HashMap<>(); Map<String, Object> fieldSchema = new HashMap<>();
@@ -37,16 +29,30 @@ public class JsonSchemaGenerator {
// 2. 어노테이션 기반 설명 추출 // 2. 어노테이션 기반 설명 추출
McpParameter paramAnnotation = field.getAnnotation(McpParameter.class); McpParameter paramAnnotation = field.getAnnotation(McpParameter.class);
JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class);
if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) { if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
fieldSchema.put("description", paramAnnotation.description()); fieldSchema.put("description", paramAnnotation.description());
} else if (descAnnotation != null && !descAnnotation.value().isEmpty()) {
fieldSchema.put("description", descAnnotation.value());
} else { } else {
fieldSchema.put("description", field.getName()); // 기본값 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); properties.put(field.getName(), fieldSchema);
} }
return properties; schema.put("properties", properties);
if (!requiredList.isEmpty()) {
schema.put("required", requiredList);
}
return schema;
} }
private static String mapJavaTypeToJsonType(Class<?> clazz) { private static String mapJavaTypeToJsonType(Class<?> clazz) {

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

@@ -0,0 +1,27 @@
package io.shinhanlife.dap.biz.mcp.tool.annotation;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
class McpFunctionAnnotationsTest {
@Test
void usesConservativeDefaultsForMcpBehaviorHints() throws NoSuchMethodException {
Method method = SampleTool.class.getDeclaredMethod("execute");
McpFunction annotation = method.getAnnotation(McpFunction.class);
assertFalse(annotation.readOnlyHint());
assertFalse(annotation.destructiveHint());
assertFalse(annotation.idempotentHint());
assertFalse(annotation.openWorldHint());
}
static class SampleTool {
@McpFunction(displayName = "sample", name = "sample", description = "sample")
void execute() {
}
}
}

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 ?\uFFFD\uB77C?\uFFFD\uB4DC ?\uFFFD\uACBD ?\uFFFD\uC6A9 ?\uFFFD\uC815
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://axhub-tool-sms.onrender.com
# EIMS 동적 라우팅 접속 정보 (Mock) # EIMS ?\uFFFD\uC801 ?\uFFFD\uC6B0???\uFFFD\uC18D ?\uFFFD\uBCF4 (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 정보 (개발 환경) --- # --- ?\uFFFD\uD55C?\uFFFD\uC774??EAI/MCI ?\uFFFD\uACC4 IP ?\uFFFD\uBCF4 (\uAC1C\uBC1C ?\uFFFD\uACBD) ---
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

@@ -12,11 +12,14 @@
</appender> </appender>
<!-- 3. 파일(File) 출력 설정 (서버 운영용) --> <!-- 3. 파일(File) 출력 설정 (서버 운영용) -->
<!-- Logback에서 시스템 Hostname을 가져오기 위한 설정 -->
<property name="HOSTNAME" value="${HOSTNAME}" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/axhub-tool-email.log</file> <!-- 현재 로그가 쌓이는 파일 --> <file>/swlog/dap-tool-email/A01/${HOSTNAME}_A01.log</file> <!-- 현재 로그가 쌓이는 파일 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 --> <!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 -->
<fileNamePattern>logs/axhub-tool-email-%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>/swlog/dap-tool-email/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.log</fileNamePattern>
<!-- 최대 30일치 로그만 보관하고 오래된 것은 자동 삭제 --> <!-- 최대 30일치 로그만 보관하고 오래된 것은 자동 삭제 -->
<maxHistory>30</maxHistory> <maxHistory>30</maxHistory>
</rollingPolicy> </rollingPolicy>

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 Onnba3011Req {
@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

@@ -1,63 +0,0 @@
package io.shinhanlife.dap.mcc.other.presentation;
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 lombok.Data;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @package io.shinhanlife.dap.mcc.other.presentation
* @className MciTestController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@RestController
@RequestMapping("/api/test/mci")
@RequiredArgsConstructor
public class MciTestController {
private final ShinhanMciSender shinhanMciSender;
@Data
public static class SampleBizData {
private String customerId;
private String amount;
private String message;
}
@PostMapping("/send")
public String testSendMci(HttpServletRequest request, @RequestBody SampleBizData bizData) {
try {
MciRequestWrapper<SampleBizData> wrapper = new MciRequestWrapper<>();
ShinhanCommonHeaderDto header = new ShinhanCommonHeaderDto();
header.setItrIfId("NCSBACO00001");
header.setRcvSvcId("OBACA9040");
wrapper.setTgrmCmnnhddValu(header);
wrapper.setBody(bizData);
String hostUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
String targetUrl = hostUrl + "/api/mock/esb/api"; // 로컬 Mock 서버 URL 호출
return shinhanMciSender.send(targetUrl, wrapper);
} catch (Exception e) {
return "MCI 전송 실패: " + e.getMessage();
}
}
}

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