3 Commits

Author SHA1 Message Date
jade
5955c2168e 소스 업데이트
All checks were successful
Deploy Tools / deploy (push) Successful in 1m6s
2026-09-17 17:32:46 +09:00
jade
244bfade74 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/ToolScaffolder.java
#	dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/converter/RecruitmentFeeDisclosureDataConverter.java
#	dat-was-pro/src/main/java/io/shinhanlife/dat/mcc/biz/pro/dto/RecruitmentFeeDisclosureDataRequest.java
#	dat-was-sal/Dockerfile
#	dat-was-sal/build.gradle
#	dat-was-sal/src/main/java/io/shinhanlife/dat/mcc/sal/DatWasSalApplication.java
#	dat-was-sal/src/main/resources/application-dev.yml
#	dat-was-sal/src/main/resources/application-local.yml
#	dat-was-sal/src/main/resources/application-prod.yml
#	dat-was-sal/src/main/resources/application-test.yml
#	dat-was-sal/src/main/resources/application.yml
#	dat-was-sal/src/main/resources/logback-spring.xml
#	dat-was-sal/src/main/resources/tool-service-manifest.yml
2026-09-17 17:32:18 +09:00
jade
a8555f05af 소스 업데이트 2026-09-17 17:31:37 +09:00
309 changed files with 80 additions and 19235 deletions

520
README.md
View File

@@ -1,504 +1,72 @@
# DATMT MCP Tool Pods
# 신한라이프 시스템 도메인 Tool Pod (dat-was-datsy)
신한라이프 업무 기능을 MCP(Model Context Protocol) Tool로 제공하는 Java 멀티 모듈 프로젝트입니다. 각 업무 모듈은 독립 실행 가능한 Spring Boot 애플리케이션이며, MCP Streamable HTTP와 REST 실행 API를 함께 제공합니다.
신한라이프 DX그룹 PJT **MCP & TOOL 파트**에서 개발 및 운영하는 시스템(SYS) 도메인 전용 DATMT Tool Service Pod(`dat-was-sys`)입니다.
IAM(계정·인증·인가 및 담당자 조회)과 PCT(점검 일정, 릴리즈 이력, 시스템 공지사항 등) 영역에서 AI Agent가 시스템 운영 현황을 파악하고 질의응답을 지원할 수 있는 MCP 도구를 제공합니다.
이 저장소에는 DATMS(Gateway) 애플리케이션이 포함되어 있지 않습니다. DATMT는 Gateway로 Tool을 push 등록하지 않으며, 각 Pod가 `GET /tool-manifest`를 제공하면 DATMS가 이 Manifest를 pull하여 Tool 목록을 구성합니다. Tool 조회와 직접 실행은 각 Pod에서도 자체적으로 처리합니다.
---
## 기술 기준
## 1. 서비스 사양 요약
- Java 21
- Gradle Wrapper 8.14.3
- Spring Boot 3.5.11
- MCP Java SDK 2.0.0
- Spring AI Community MCP Annotations 0.9.0
- Jackson 2.19.4 (Spring Boot BOM)
- MapStruct, MyBatis, Redis, Kafka, Resilience4j
- JUnit 5
| 항목 | 내용 |
|---|---|
| **서비스명 (모듈)** | `dat-was-sys` (`dat-was-datsy`) |
| **서버 포트** | **8086** (기본) |
| **MCP 엔드포인트** | `http://localhost:8086/mcp`, `http://localhost:8086/mcp/message` |
| **Swagger UI** | `http://localhost:8086/swagger-ui.html` |
| **Bundle ID** | `was-sys` |
| **기반 기술** | Java 21, Spring Boot 3.5.11, MCP Java SDK 2.0.0 |
| **공통 의존성** | `io.shinhanlife:dat-lib-datmt:0.0.1-SNAPSHOT` (`dat-was-lib`) |
## 모듈 구성
---
| 모듈 | 역할 | 기본 포트 | Tool 수 |
|---|---|---:|---:|
| `dat-was-lib` | MCP 서버, Tool 스캔·실행, Schema, Manifest, 보안, MCI/EAI/HTTP 연동 공통 기능 | - | - |
| `dat-was-cus` | 고객·CRM·VOC·웹 콘텐츠 관리 Tool | 8084 | 51 |
| `dat-was-sal` | 영업·청구·인수·동의·현장지원 Tool | 8082 | 52 |
| `dat-was-pro` | 상품 영역 Tool(개인고객 상세조회·모집수수료 공시) | 8085 | 2 |
| `dat-was-sys` | IAM·시스템 상태·공지·점검·배포 Tool | 8086 | 51 |
## 2. 제공 MCP 도구 (Tools) 목록
업무 모듈에는 총 156개의 `@McpTool` 선언이 있습니다. 현재 업무 모듈의 `src/main/resources`에는 별도 `tool-definitions` YAML이 없으며, Tool 메타데이터는 어노테이션과 `@GrowToolHint`를 기준으로 생성됩니다.
### ① IAM (Identity & Access Management / 조직)
- **`AccountInquiryUseCase`**: 사번 또는 계정 ID를 통한 사용자/임직원 기본 정보 조회
- **`AuthenticationInquiryUseCase`**: 계정의 2차 인증 상태, 계정 잠김 여부 등 인증 상태 확인
- **`AuthorizationInquiryUseCase`**: 사용자가 보유한 시스템 권한 그룹 및 리소스 접근 인가 현황 조회
- **`SystemStatusUseCase`**: 신한라이프 주요 IT 서비스 및 연계 채널의 실시간 가동 상태 점검
- **`TeamContactUseCase`**: 특정 업무/시스템별 담당 부서, 담당자 및 비상 연락처 조회
현재 업무 구현은 개발·연동 검증 단계입니다. `dat-was-sal``cmm_claim_search``cmm_memo_retriever`는 각각 MCI와 HTTP Client 흐름을 사용하며, 나머지 Tool은 외부 시스템을 변경하지 않는 모의 응답을 중심으로 구현되어 있습니다.
### ② PCT (Process & Change Management / 운영)
- **`MaintenanceInquiryUseCase`**: 예정된 시스템 정기 점검, 서버 패치 및 서비스 중단 일정 조회
- **`ReleaseInquiryUseCase`**: 최근 배포된 시스템 기능 개선 및 릴리즈 이력 정보 조회
- **`SystemNoticeUseCase`**: 전사 시스템 공지사항 및 긴급 장애 공지 내역 조회
## 처리 구조
---
## 3. 공통 라이브러리 연동 (Composite Build)
본 저장소는 상위 워크스페이스의 `../dat-lib-datmt` 공통 라이브러리를 로컬 복합 빌드(Composite Build)로 직접 참조합니다.
```text
MCP Client 또는 REST Client
├─ MCP Streamable HTTP: /mcp
└─ REST: POST /mcp/{toolName}
Tool Pod
├─ McpToolMethodRegistry
│ └─ Spring Bean의 @McpTool 메서드 탐색 및 실행 메서드 캐시
├─ ToolRegistryHeartbeatSender
│ ├─ Tool 메타데이터 생성
│ └─ 선택적 tool-definitions YAML 병합
├─ ToolManifestService
│ └─ DATMS가 pull할 bundle 단위 Manifest 생성
├─ McpToolExecutionService
│ ├─ 입력 Schema 검증
│ ├─ 요청 DTO 변환 및 Tool 호출
│ └─ 출력 Schema 검증
└─ UseCase → Converter → MCI/EAI/HTTP Client 또는 Mock 응답
workspace/
├── dat-lib-datmt/ # 공통 라이브러리 (:dat-was-lib)
└── dat-was-datsy/ # [현재 저장소] 시스템 도메인 Tool Pod (:dat-was-sys)
```
애플리케이션 시작 시 다음 순서로 Tool이 준비됩니다.
---
1. `ToolDefinitionRepository``classpath*:tool-definitions/**/*.yml` 경로의 정의가 있을 경우 이를 읽고 V17 필수 항목을 검증합니다.
2. `ToolRegistryHeartbeatSender``@McpTool` 메서드를 스캔하고, 존재하는 YAML 정의가 있으면 병합해 `ToolMetadata`를 생성합니다.
3. `McpToolMethodRegistry`가 실제 호출 가능한 Bean과 메서드를 Tool 이름으로 캐시합니다.
4. `ToolPodMcpToolSynchronizer`가 Tool을 MCP SDK 서버에 등록합니다.
5. REST와 MCP 요청은 공통 `McpToolExecutionService`를 통해 실행됩니다.
DATMT 내부에는 `/registry/register`, `/registry/deregister` 호출이나 주기적인 Gateway heartbeat 전송이 없습니다. `ToolRegistryHeartbeatSender`라는 클래스명은 호환성을 위해 남아 있지만 현재 역할은 로컬 Tool 스캔과 메타데이터 생성뿐입니다.
### DATMS 연동 방식
```text
DATMS
└─ GET {DATMT Pod URL}/tool-manifest
└─ bundleId + revision + tools[]
└─ 각 Tool endpoint: {Pod URL}/mcp/{toolName}
```
- `mcp.manifest.bundle-id`는 Manifest를 제공하는 Pod의 고유 식별자이며 필수입니다.
- 현재 값은 `was-sal`, `was-cus`, `was-pro`, `was-sys`입니다.
- DATMS에 설정한 bundle ID와 DATMT가 반환하는 `bundleId`가 일치해야 같은 Tool bundle로 관리됩니다.
- `mcp.manifest.name-prefix`가 비어 있지 않으면 모든 Tool 이름이 해당 prefix로 시작해야 합니다.
- Manifest 내용이 바뀌면 `revision`이 증가하며, `If-None-Match`가 일치하면 `304 Not Modified`를 반환합니다.
## 제공 API
각 업무 Pod가 동일한 API 구조를 제공합니다.
| 목적 | 메서드 | 경로 |
|---|---|---|
| MCP Streamable HTTP | MCP 프로토콜 | `/mcp` |
| MCP 메시지 전송 | MCP 프로토콜 | `/mcp/message` |
| 로컬 Tool 메타데이터 조회 | `GET` | `/mcp/api/v1/tools/local` |
| Tool 직접 실행 | `POST` | `/mcp/{toolName}` |
| Tool Manifest 조회 | `GET` | `/tool-manifest` |
| 카테고리별 Tool Manifest 조회 | `GET` | `/tool-manifest/{categoryKey}` |
`GET /tool-manifest``If-None-Match` 요청 헤더를 지원합니다. Manifest가 변경되지 않았으면 `304 Not Modified`를 반환합니다.
### REST 실행 예시
다음은 SYS Pod의 시스템 상태 Tool 호출 예시입니다.
## 4. 빌드 및 실행 가이드
### 컴파일 및 빌드
```powershell
$headers = @{
'X-Tool-Server-API-Key' = $env:TOOL_SERVER_API_KEY
'X-Guid' = 'guid-local-001'
'X-Praf-No' = '100001'
'X-Request-Id' = 'request-local-001'
'X-Request-Time' = '2026-08-25T12:34:56+09:00'
'X-Vrtl-Praf-No' = 'V100001'
'X-App-Code' = 'DATMT'
'X-Project-Code' = 'AXHUB'
'X-User-Ip' = '10.0.0.10'
'X-Caller-Ip' = '10.0.0.20'
'X-Caller-Host' = 'caller.example.internal'
'X-Channel' = 'MCP'
'X-Agent-Id' = 'agent-local-001'
'mcp-session-id' = 'session-local-001'
}
Invoke-RestMethod `
-Method Post `
-Uri 'http://localhost:8086/mcp/iam_system_status' `
-Headers $headers `
-ContentType 'application/json' `
-Body '{"environment":"개발"}'
.\gradlew.bat compileJava
```
### 요청 헤더 계약
DATMS가 DATMT Tool Service를 호출할 때 사용하는 헤더는 다음과 같습니다. HTTP 헤더 이름은 대소문자를 구분하지 않지만, 문서와 구현에서는 아래 표기를 기준으로 사용합니다.
| 헤더 | 필수 여부 | 용도 | 전달 동작 |
|---|---|---|---|
| `X-Tool-Server-API-Key` | 인증 설정 시 필수 | DATMS와 DATMT 사이의 Tool Server 인증 | `mcp.security.api-key` 또는 `api-keys`와 비교 |
| `X-Guid` | 선택 | 업무 호출 상관관계 식별자 | 실행 로그, 성공 응답, 하위 HTTP 호출로 전달 |
| `X-Praf-No` | 선택 | 실제 사용자 사번 | 세션 조회와 하위 HTTP 호출로 전달 |
| `X-Request-Id` | 선택 | 요청 추적 식별자 | 실행 로그, 성공 응답, 하위 HTTP 호출로 전달 |
| `X-Request-Time` | 선택 | 요청 발생 시각 | 하위 HTTP 호출로 전달 |
| `X-Vrtl-Praf-No` | 선택 | 가상 사용자 사번 | 하위 HTTP 호출로 전달 |
| `X-App-Code` | 선택 | 호출 애플리케이션 코드 | 하위 HTTP 호출로 전달 |
| `X-Project-Code` | 선택 | 호출 프로젝트 코드 | 하위 HTTP 호출로 전달 |
| `X-User-Ip` | 선택 | 사용자 IP 주소 | 하위 HTTP 호출로 전달 |
| `X-Caller-Ip` | 선택 | 호출 시스템 IP 주소 | 하위 HTTP 호출로 전달 |
| `X-Caller-Host` | 선택 | 호출 시스템 호스트명 | 하위 HTTP 호출로 전달 |
| `X-Channel` | 선택 | 호출 채널 | 하위 HTTP 호출로 전달 |
| `X-Agent-Id` | 선택 | 호출 Agent 식별자 | 하위 HTTP 호출로 전달 |
| `mcp-session-id` | 선택 | MCP 세션 식별자 | 성공 응답과 하위 HTTP 호출로 전달 |
`McpRequestHeaderFilter`는 URI에 `/mcp`가 포함된 요청에서 위 헤더를 `McpRequestHeaderContext`에 저장하므로 REST `/mcp/{toolName}`과 MCP Streamable HTTP `/mcp`, `/mcp/message`에 모두 적용됩니다. REST 경로는 `BusinessToolController`도 동일한 헤더를 직접 읽어 실행 서비스에 전달합니다. 헤더가 없는 하위 HTTP 호출에는 `X-ANONYMOUS-REQ: AXHUB-TOOL`이 설정됩니다.
기존 `guid`, `employee-no`, `virtual-employee-no` 헤더는 지원하지 않습니다.
주요 실행 응답은 다음과 같습니다.
| HTTP 상태 | 코드 | 의미 |
|---:|---|---|
| 200 | - | Tool 실행 성공 |
| 404 | `TOOL_NOT_FOUND` | 요청한 Tool 이름이 없음 |
| 422 | `INVALID_PARAM` | 요청이 입력 Schema와 일치하지 않음 |
| 500 | `INVALID_TOOL_RESPONSE` | 결과가 출력 Schema와 일치하지 않음 |
| 502 | `TOOL_ERROR` | Tool 실행 중 예외 발생 |
## Tool 구현 방식
호출 가능한 메서드는 Spring AI Community의 `@McpTool`로 선언합니다. 프로젝트 고유 실행·표시 정보는 `@GrowToolHint`로 보완합니다.
```java
@McpTool(
name = "iam_system_status",
title = "시스템 상태 조회",
description = "모의 시스템 상태 정보를 조회합니다.",
annotations = @McpTool.McpAnnotations(openWorldHint = false)
)
@GrowToolHint(
categoryKey = "iam",
mappingId = "DIRECT_IAM_STATUS",
requiresApproval = false,
timeoutMillis = 5000L,
retryMaxAttempts = 3
)
SystemStatusResponse getSystemStatus(SystemStatusRequest request);
```
`@GrowToolHint`의 실행 제어 기본값은 `timeoutMillis = 5000L`, `retryMaxAttempts = 3`입니다. 어노테이션에서 값을 지정하면 해당 Tool의 메타데이터에 반영됩니다. `retryEnabled`는 지원하지 않으며, 재시도 여부는 `retryMaxAttempts` 값으로 판단합니다.
`@McpTool.name`은 다음 형식을 사용합니다.
```text
^[a-z][a-z0-9_]{2,63}$
```
예: `cmm_claim_search`, `crm_customer_detail`, `iam_system_status`
이름 중복과 형식은 `validateMcpToolNames` Gradle 작업으로 검사합니다. 현재 루트 `build.gradle`에는 `validateToolSchemaV17` Gradle 작업이 등록되어 있지 않으며, 모든 `bootJar` 작업은 `validateMcpToolNames`에만 의존합니다.
## Tool YAML 정의
공통 라이브러리는 Tool 메타데이터를 보강하기 위한 선택적 YAML 정의를 지원합니다. YAML 정의를 추가할 경우 업무 모듈의 다음 경로를 사용합니다.
```text
dat-was-*/src/main/resources/tool-definitions/{category}/{tool-name}.yml
```
현재 업무 모듈에는 이 경로의 YAML 정의가 없습니다. 따라서 실행 시 메타데이터는 `@McpTool`, `@GrowToolHint`, DTO Schema를 기준으로 구성됩니다. YAML 정의를 도입하는 경우 V17 주요 필수 항목은 다음과 같습니다.
- `name`, `display_name`, `version`, `category_key`
- `description.function`, `when_to_use`, `when_not_to_use`, `io_limits`
- 3~10개의 `example_queries`
- `read_only`, `destructive`, `idempotent`
- `parameters_schema.type: object`
- `parameters_schema.additionalProperties: false`
- 각 입력 property의 `description`
입력·출력 Schema는 `ToolSchemaResolver`가 어노테이션의 Schema 리소스와 인라인 Schema, DTO에서 생성한 Schema를 해석합니다. YAML 정의가 있으면 `ToolRegistryHeartbeatSender``parameters_schema``output_schema`를 병합하여 최종 메타데이터를 만듭니다.
### Tool 실행 제어 메타데이터
다음 메타데이터는 Tool 목록과 Manifest에 함께 제공됩니다.
| 필드 | 기본값 | 설명 |
|---|---:|---|
| `timeoutMillis` | `5000` | Gateway가 Tool 응답을 기다리는 최대 시간(밀리초) |
| `retryMaxAttempts` | `3` | 최초 호출을 포함한 최대 시도 횟수 |
`retryEnabled` 필드는 외부 메타데이터와 `ToolMetadata`에서 제거되었습니다. 따라서 클라이언트와 Gateway는 `retryMaxAttempts`만 사용해야 하며, 값이 없거나 1보다 작으면 기본값 3이 적용됩니다. Scaffold로 생성되는 Tool에도 위 두 값이 자동으로 삽입됩니다.
표준 MCP `tools/list` 응답에서는 두 필드가 Tool의 `_meta`에 camelCase로 내려갑니다.
```json
{
"_meta": {
"timeoutMillis": 5000,
"retryMaxAttempts": 3
}
}
```
`GET /tool-manifest` 응답의 각 Tool `_meta`에도 동일한 두 필드가 포함됩니다. `GET /mcp/api/v1/tools/local` 응답은 내부 `ToolMetadata` 표현을 사용하므로 동일한 실행 제어 값을 확인할 수 있습니다.
실행 시 Schema 검증은 MCP Java SDK의 `DefaultJsonSchemaValidator`를 사용하며 JSON Schema 2020-12 기준으로 처리합니다. 입력 불일치는 `422 INVALID_PARAM`, 출력 불일치는 `500 INVALID_TOOL_RESPONSE`로 반환됩니다. 단, Schema 해석 또는 검증기 자체에서 예외가 발생하면 현재 구현은 오류를 로그에 기록하고 해당 검증을 건너뜁니다.
## 로컬 실행
### 사전 조건
- JDK 21
- 프로젝트에 포함된 Gradle Wrapper
- Redis 또는 외부 연동이 필요한 경우 Docker
기본 활성 프로필은 `local`입니다. 로컬 프로필은 H2 메모리 DB와 P6Spy를 사용합니다.
### 단위 테스트 실행
```powershell
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:TOOL_SERVER_API_KEY = 'tool-server-key'
.\gradlew.bat test
```
`TOOL_SERVER_API_KEY`를 지정하지 않으면 현재 개발 기본값인 `tool-server-key`가 사용됩니다. 운영 환경에서는 기본값을 사용하지 말고 DATMS의 Tool Server API Key와 동일한 별도 Secret을 주입해야 합니다.
각 Pod는 별도 터미널에서 실행합니다.
### 로컬 애플리케이션 실행
```powershell
# 영업 Tool Pod
$env:AXHUB_TOOL_URL = 'http://localhost:8082'
.\gradlew.bat :dat-was-sal:bootRun
# 고객 Tool Pod
$env:AXHUB_TOOL_URL = 'http://localhost:8084'
.\gradlew.bat :dat-was-cus:bootRun
# 상품 Tool Pod
$env:AXHUB_TOOL_URL = 'http://localhost:8085'
.\gradlew.bat :dat-was-pro:bootRun
# 시스템 Tool Pod
$env:AXHUB_TOOL_URL = 'http://localhost:8086'
.\gradlew.bat :dat-was-sys:bootRun
```
*기본 활성화 프로파일: `local` (`application-local.yml` 참조)*
실행 후 SYS Pod 기준 확인 URL은 다음과 같습니다.
```text
http://localhost:8086/mcp/api/v1/tools/local
http://localhost:8086/tool-manifest
http://localhost:8086/swagger-ui/index.html
```
### 주요 환경 변수
| 환경 변수 | 설명 | 기본값 |
|---|---|---|
| `SPRING_PROFILES_ACTIVE` | Spring 활성 프로필 | `local` |
| `PORT` | Pod 수신 포트 | 모듈별 기본 포트 |
| `TOOL_SERVER_API_KEY` | DATMS가 `X-Tool-Server-API-Key`로 전달할 공통 인증 Key | `tool-server-key` |
| `AXHUB_TOOL_URL` | Manifest의 Tool endpoint 생성에 사용할 Pod 외부 URL | `http://localhost:${server.port}` |
| `AXHUB_GATEWAY_URL` | 프로필 및 Compose 호환용 Gateway URL. 현재 DATMT의 push 등록에는 사용하지 않음 | `http://localhost:8081` |
| `SPRING_DATA_REDIS_HOST` | Redis 호스트 | `localhost` |
| `SPRING_DATA_REDIS_PORT` | Redis 포트 | `6379` |
| `GLOW_COMMUNICATION_MCI_HOST` | MCI 대상 호스트 | 프로필별 설정 |
| `GLOW_COMMUNICATION_MCI_PORT` | MCI 대상 포트 | 프로필별 설정 |
`mcp.security.api-key`는 단일 DATMS 공통 Key를, `mcp.security.api-keys``API Key → tenant ID` 형태의 다중 Key를 지원합니다. `ApiKeyInterceptor`는 Spring MVC가 처리하는 `/rpc/**`, `/mcp/**` 요청에 적용되므로 REST `/mcp/{toolName}``/mcp/api/v1/tools/local`은 올바른 `X-Tool-Server-API-Key`가 없으면 `401 Unauthorized`가 됩니다. 두 설정이 모두 비어 있을 때는 해당 MVC 요청을 익명으로 허용합니다. 현재 네 업무 Pod의 기본 `application.yml`은 단일 Key를 설정합니다.
반면 `/mcp``/mcp/message`는 별도 Servlet으로 등록되어 Spring MVC `HandlerInterceptor`를 통과하지 않습니다. 현재 구현만으로는 이 두 MCP Streamable HTTP 경로에 `ApiKeyInterceptor` 인증이 적용되지 않으므로, 운영 배포 전 Servlet Filter 또는 전용 MCP 인증 계층을 추가해야 합니다.
## Scaffold
공통 라이브러리는 새 Tool과 새 Tool Pod를 만드는 두 개의 Java CLI를 제공합니다. 두 클래스의 `main` 메서드를 IDE에서 실행하거나 필요한 인자를 전달해 실행할 수 있습니다.
| Scaffolder | 역할 | 주요 생성·수정 대상 |
|---|---|---|
| `ToolScaffolder` | 기존 Pod에 Tool 구현 추가 | UseCase, DTO, Converter, Mock 응답, V17 Tool YAML, 연동 설정 |
| `PodScaffolder` | 새 실행 Pod 모듈 추가 | 모듈 디렉터리, `build.gradle`, Dockerfile, Application 클래스, 프로필 설정, `settings.gradle`, `docker-compose.yml` |
`PodScaffolder`의 인자 순서는 다음과 같습니다.
```text
PodScaffolder <module-name> <port> [author] [yyyy.MM.dd]
```
예를 들어 `payment 8099`를 입력하면 `dat-was-payment` 모듈을 생성합니다. 생성되는 `application.yml`에는 다음 계약이 포함됩니다.
```yaml
mcp:
manifest:
bundle-id: dat-was-payment
name-prefix: ""
security:
api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
```
생성되는 Compose 서비스에도 `TOOL_SERVER_API_KEY=${TOOL_SERVER_API_KEY:-tool-server-key}`가 추가됩니다. 생성 후에는 다음 항목을 반드시 확인해야 합니다.
1. `bundle-id`를 DATMS에 등록할 bundle ID와 일치시킵니다.
2. 운영 환경의 `TOOL_SERVER_API_KEY`를 DATMS가 전달하는 Key와 동일한 Secret으로 설정합니다.
3. 실제 배포 주소에 맞게 `AXHUB_TOOL_URL`을 설정합니다.
4. MCI·EAI·HTTP 연동 대상과 timeout을 환경별 설정으로 교체합니다.
5. `validateMcpToolNames`와 모듈·전체 테스트를 실행합니다. YAML 정의를 추가했다면 `ToolSchemaV17ValidationRunner` 또는 관련 테스트로 V17 항목을 별도 검증합니다.
## 테스트와 검증
### 도커 컨테이너 빌드 및 로컬 구동
```powershell
# 운영 소스 전체 컴파일
.\gradlew.bat classes
# 전체 테스트
.\gradlew.bat test
# 모듈별 테스트
.\gradlew.bat :dat-was-lib:test
.\gradlew.bat :dat-was-cus:test
.\gradlew.bat :dat-was-sal:test
.\gradlew.bat :dat-was-pro:test
.\gradlew.bat :dat-was-sys:test
# Tool 이름·중복 검사
.\gradlew.bat validateMcpToolNames
# 검증, 테스트, 패키징
.\gradlew.bat clean build
docker compose -f docker-compose.local.yml up -d --build
```
### 실행 전 확인
테스트 수와 성공 여부는 소스 변경에 따라 달라지므로 고정된 수치를 문서화하지 않습니다. 배포 전 현재 작업 트리에서 다음 명령을 실행해 확인합니다.
```powershell
.\gradlew.bat test validateMcpToolNames
```
## Docker Compose
`docker-compose.yml`은 DATMT의 네 업무 Pod만 정의합니다.
| 서비스 | 컨테이너 포트 | 호스트 포트 |
|---|---:|---:|
| `was-sal` | 8082 | 8282 |
| `was-cus` | 8084 | 8284 |
| `was-pro` | 8085 | 8285 |
| `was-sys` | 8086 | 8286 |
모듈 Dockerfile은 사전에 생성된 Boot JAR를 이미지에 복사합니다. 먼저 JAR를 빌드한 뒤 Compose를 실행합니다.
```powershell
.\gradlew.bat :dat-was-sal:bootJar :dat-was-cus:bootJar :dat-was-pro:bootJar :dat-was-sys:bootJar
docker compose up --build
```
Compose 파일의 용도와 현재 주의점은 다음과 같습니다.
| 파일 | 용도 | 현재 소스 기준 주의점 |
|---|---|---|
| `docker-compose.yml` | DATMT 네 Pod 단독 실행 | `gateway` 서비스가 없지만 push 등록이 제거되어 DATMT 시작에는 필요하지 않음 |
| `docker-compose.local.yml` | DATMS와 DATMT의 로컬 통합 구성 | 두 저장소가 같은 상위 디렉터리에 있는 구조를 가정 |
| `docker-compose.prod.yml` | DATMS와 DATMT의 개발 프로필 기반 OCI 구성 | 저장소의 runner 등록 토큰을 운영 Secret으로 분리해야 함 |
`docker-compose.local.yml``docker-compose.prod.yml`의 build context는 각각 `./dat-was-DATMS`, `./dat-was-datmt`입니다. 현재 파일 위치에서 사용할 때는 context 기준을 두 저장소의 상위 디렉터리로 맞춰야 합니다.
```powershell
# DATMT 저장소 디렉터리에서 실행
docker compose --project-directory .. -f docker-compose.local.yml up --build
```
현재 기존 네 Pod의 Compose 정의에는 `TOOL_SERVER_API_KEY` 환경 변수 전달이 없습니다. 따라서 컨테이너는 애플리케이션 기본값 `tool-server-key`를 사용합니다. 운영 배포 전 각 서비스에 Secret 기반 `TOOL_SERVER_API_KEY` 전달 설정을 추가하고 DATMS의 Key와 일치시켜야 합니다. DATMS는 각 Pod의 `AXHUB_TOOL_URL` 또는 배포 URL에 접근해 `/tool-manifest`를 pull할 수 있어야 합니다.
## OpenShift (Kubernetes) 배포
OpenShift 개발 환경용 Kustomize 매니페스트는 [`k8s/`](k8s/)에 있습니다. 대상은 DATMT의 네 Tool Pod뿐이며 DATMS(Gateway)의 Deployment·Service·Route는 이 저장소에서 만들지 않습니다.
```text
k8s/
├─ base/ # 네 Pod 공통 ConfigMap, Service, Deployment
└─ overlays/dev/ # 개발 namespace, Registry 이미지 경로와 tag
```
### 배포 구조
각 Tool Pod는 Deployment 1개와 외부에 노출되지 않는 `ClusterIP` Service 1개를 사용합니다. OpenShift Route와 LoadBalancer Service는 생성하지 않으며, Gateway가 클러스터 내부 DNS로 호출합니다.
| Service | Pod 포트 | Gateway 호출 주소 |
|---|---:|---|
| `was-sal` | 8082 | `http://was-sal:8082` |
| `was-cus` | 8084 | `http://was-cus:8084` |
| `was-pro` | 8085 | `http://was-pro:8085` |
| `was-sys` | 8086 | `http://was-sys:8086` |
Gateway가 다른 namespace에 있으면 `was-sal.axhub-datmt-dev.svc`와 같은 FQDN을 사용하고, NetworkPolicy에서 Gateway namespace의 ingress를 별도로 허용해야 합니다.
### 반영 전 설정
다음 값은 실제 신한라이프 개발망 값으로 교체해야 합니다.
1. [`k8s/overlays/dev/kustomization.yaml`](k8s/overlays/dev/kustomization.yaml)의 namespace, 내부 Container Registry 경로, 배포 image tag
2. [`k8s/base/configmap.yaml`](k8s/base/configmap.yaml)의 `CHANGE_ME` MCI·EXTMCI·EAI 호스트
3. 실제 Secret 값
`datmt-runtime-secrets` Secret은 Git에 저장하지 않고 OpenShift namespace에서 별도로 생성합니다. 최소한 `TOOL_SERVER_API_KEY`는 DATMS가 전달하는 `X-Tool-Server-API-Key`와 같은 값이어야 합니다. DB 계정·비밀번호, API Key, 인증서 비밀번호 등도 이 Secret으로 관리합니다.
```powershell
# runtime-secrets.env는 저장소 밖에 보관합니다.
oc -n axhub-datmt-dev create secret generic datmt-runtime-secrets `
--from-env-file=runtime-secrets.env
```
Secret이 없으면 각 Deployment의 `envFrom.secretRef`를 해석할 수 없어 Pod가 시작하지 않을 수 있습니다.
### 이미지 빌드와 배포
모듈 Dockerfile은 미리 생성된 Boot JAR를 복사하므로, 이미지를 만들기 전에 네 모듈의 JAR를 빌드합니다. 개발망에서는 JDK/JRE 베이스 이미지와 Gradle/Maven 의존성을 내부 Registry·Nexus에서 사용할 수 있어야 합니다.
```powershell
.\gradlew.bat :dat-was-sal:bootJar :dat-was-cus:bootJar :dat-was-pro:bootJar :dat-was-sys:bootJar
# OpenShift 로그인 및 project 선택 후
oc kustomize k8s/overlays/dev
oc apply -k k8s/overlays/dev
oc get deployment,pod,svc -n axhub-datmt-dev
```
적용 전에는 `oc kustomize k8s/overlays/dev | oc apply --dry-run=client -f -`로 서버 측 스키마 검증을 수행합니다. 현재 매니페스트의 readiness/liveness probe는 TCP 포트 확인 방식입니다. Actuator health endpoint를 추가한 뒤에는 HTTP readiness/liveness probe로 변경하는 것을 권장합니다.
상세한 명령과 Gateway 연결 확인 방법은 [`k8s/README.md`](k8s/README.md)를 참고합니다.
## 보안 및 운영 주의사항
현재 구현을 운영 환경에 노출하기 전에 아래 항목을 반드시 점검해야 합니다.
- API Key 인터셉터는 Spring MVC의 `/rpc/**`, `/mcp/**` Handler에 적용됩니다. REST `/mcp/{toolName}``/mcp/api/v1/tools/local`은 인증 대상입니다.
- 별도 Servlet인 `/mcp`, `/mcp/message`는 MVC 인터셉터를 우회하므로 현재 API Key 인증 대상이 아닙니다. 운영 노출 전에 별도 인증을 추가해야 합니다.
- `/tool-manifest``/tool-manifest/{categoryKey}`는 위 인터셉터 경로 밖에 있어 현재 API Key 인증 대상이 아닙니다. 내부망·Ingress 정책 또는 별도 인증이 필요한지 운영 기준을 확인해야 합니다.
- 네 업무 Pod의 기본 Key는 모두 `tool-server-key`입니다. 운영에서는 반드시 별도 Secret으로 교체하고 DATMS의 `X-Tool-Server-API-Key` 값과 일치시켜야 합니다.
- 설정된 단일 Key와 다중 Key가 모두 없을 때만 익명 요청이 허용됩니다.
- `mcp.security.tenant-domains`는 설정 객체에 바인딩되지만 Tool별 인가에 사용되지 않습니다.
- `requiresApproval`은 메타데이터에만 기록되며 실행 차단이나 승인 확인 로직은 없습니다.
- 입력·출력 Schema 처리 자체에서 예외가 발생하면 현재 실행 서비스는 로그를 남기고 검증을 건너뜁니다.
- CORS는 모든 Origin을 허용하면서 credential도 허용하도록 설정되어 있습니다. 운영 Origin을 명시적으로 제한해야 합니다.
- `docker-compose.prod.yml`에 runner 등록 토큰이 평문으로 포함되어 있습니다. 사용 중인 토큰은 폐기·재발급하고 배포 Secret으로 이전해야 합니다.
- Tool 요청과 연동 오류 로그에 개인정보나 인증정보가 포함되지 않도록 DTO와 로그 마스킹 정책을 검토해야 합니다.
## 주요 소스 위치
| 주제 | 위치 |
|---|---|
| 공통 빌드 및 검증 작업 | `build.gradle` |
| REST Tool 실행 API | `dat-was-lib/src/main/java/io/shinhanlife/dat/mcc/presentation/BusinessToolController.java` |
| 요청 헤더 캡처·전달 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/McpRequestHeaderFilter.java`, `McpRequestHeaderContext.java` |
| Tool 실행 서비스 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/McpToolExecutionService.java` |
| JSON Schema 2020-12 검증 설정 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/config/ToolSchemaConfiguration.java` |
| 실행 메서드 Registry | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/McpToolMethodRegistry.java` |
| MCP SDK 서버 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/ToolMcpServerConfiguration.java` |
| MCP Tool 동기화 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/ToolPodMcpToolSynchronizer.java` |
| Tool 스캔 및 메타데이터 생성 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/ToolRegistryHeartbeatSender.java` |
| MCP `tools/list` 메타데이터 변환 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/ToolMetadataMcpMapper.java` |
| YAML Tool 정의 로딩 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/metadata/ToolDefinitionRepository.java` |
| V17 정의 검증 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/metadata/ToolDefinitionValidator.java` |
| Manifest 생성 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest/ToolManifestService.java` |
| API Key 인터셉터 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/security/ApiKeyInterceptor.java` |
| Tool·Pod 스캐폴딩 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/ToolScaffolder.java`, `PodScaffolder.java` |
## 개발 시 권장 확인 순서
1. Tool 메서드와 요청·응답 DTO를 구현합니다.
2. 동일 이름의 V17 YAML 정의를 `tool-definitions` 아래에 추가합니다.
3. `validateMcpToolNames`와 모듈·전체 테스트를 실행합니다. Tool YAML 정의를 추가한 경우에는 V17 항목을 별도 검증합니다.
4. 모듈 테스트와 전체 테스트를 실행합니다.
5. 로컬 Pod에서 `/mcp/api/v1/tools/local``/tool-manifest`를 확인합니다.
6. DATMS의 bundle ID, Pod Manifest URL, Tool Server API Key가 DATMT 설정과 일치하는지 확인합니다.
7. REST와 MCP 양쪽에서 동일한 Tool 결과·요청 헤더 전달·오류 계약을 확인합니다.

View File

@@ -34,6 +34,15 @@ subprojects {
}
repositories {
// [로컬 개발 순서]
// 1. ../dat-lib-datmt에서 .\gradlew.bat :dat-was-lib:publishToMavenLocal 실행
// 2. 위 명령이 ~/.m2/repository/io/shinhanlife/dat-lib-datmt에 JAR/POM을 등록
// 3. 아래 mavenLocal()이 등록된 JAR을 찾아 implementation 의존성을 해석
//
// [신한라이프 이관]
// mavenLocal() 대신 사내 Nexus Maven repository를 추가합니다.
// implementation 좌표는 그대로 유지합니다.
mavenLocal()
// --------------------------------------------------------------------
// 외부 개발 환경
@@ -167,80 +176,3 @@ subprojects {
useJUnitPlatform()
}
}
// ============================================================================
// Tool Validation
// ============================================================================
// Tool Annotation / Tool Schema 검증 프로그램이 들어있는
// 공통 라이브러리 프로젝트입니다.
def toolCoreProject = project(':dat-was-lib')
// ============================================================================
// MCP Tool Name Validation
// ============================================================================
// 전체 Tool Pod 프로젝트에서 선언된
// @McpTool(name = "...") 값의 중복 여부를 검사합니다.
//
// 동일한 Tool Name이 존재하면 배포 산출물 생성 전에
// 빌드를 실패시켜 중복 Tool이 배포되는 것을 차단합니다.
tasks.register('validateMcpToolNames', JavaExec) {
// Gradle Verification Task 그룹으로 분류합니다.
group = 'verification'
// Gradle Tasks 조회 시 표시되는 설명입니다.
description = 'Checks duplicate @McpTool names across all Tool modules before packaging.'
// 검사 프로그램이 dat-was-lib에 존재하므로
// JavaExec 실행 전에 dat-was-lib의 classes 작업을 수행합니다.
dependsOn toolCoreProject.tasks.named('classes')
// dat-was-lib의 Runtime Classpath를
// Tool Validation Runner 실행 Classpath로 사용합니다.
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
// Tool Name 중복 검증을 수행하는 Main Class입니다.
mainClass.set(
'io.shinhanlife.dat.lib.validation.McpToolNameValidationRunner'
)
// 전체 멀티모듈 프로젝트를 검사할 수 있도록
// Root Project 경로를 Runner의 Argument로 전달합니다.
args rootProject.projectDir.absolutePath
}
// ============================================================================
// BootJar Validation Hook
// ============================================================================
subprojects {
// Spring Boot 실행 모듈에서 bootJar를 수행할 경우
// 실제 배포 JAR이 만들어지기 전에
//
// 1. MCP Tool Name 중복 검사
// 2. Tool Schema V17 검사
//
// 두 검증 작업을 반드시 수행하도록 합니다.
tasks.matching { it.name == 'bootJar' }.configureEach {
// @McpTool Name 중복 검증
dependsOn rootProject.tasks.named('validateMcpToolNames')
}
}
bootJar {
enabled = false
}
jar {
enabled = false
}

View File

@@ -1,8 +0,0 @@
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
COPY dat-was-cus/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8084
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -1,10 +0,0 @@
plugins {
// OTH Tool Pod를 독립 실행 가능한 Spring Boot JAR로 생성합니다.
id 'org.springframework.boot'
}
dependencies {
// MCP Server, Tool 공통 처리, MCI/EAI 연동 기반은 dat-was-lib에서 상속합니다.
implementation project(':dat-was-lib')
implementation 'org.apache.poi:poi-ooxml:5.3.0'
}

View File

@@ -1,34 +0,0 @@
package io.shinhanlife.dat.mcc.biz.crm.dto;
import lombok.Data;
import org.springaicommunity.mcp.annotation.McpToolParam;
@Data
public class CrmToolRequest {
@McpToolParam(description = "업무 대상 식별자", required = false)
private String subjectId;
@McpToolParam(description = "고객 식별자", required = false)
private String customerId;
@McpToolParam(description = "검색어 또는 처리 내용", required = false)
private String content;
@McpToolParam(description = "상태, 유형, 등급 또는 처리 값", required = false)
private String value;
@McpToolParam(description = "담당자 식별자", required = false)
private String assigneeId;
@McpToolParam(description = "조회 시작일(YYYY-MM-DD)", required = false)
private String startDate;
@McpToolParam(description = "조회 종료일 또는 처리 예정일(YYYY-MM-DD)", required = false)
private String endDate;
@McpToolParam(description = "페이지 번호", required = false)
private Integer page;
@McpToolParam(description = "페이지 크기", required = false)
private Integer size;
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.dat.mcc.biz.crm.dto;
import java.util.List;
import lombok.Data;
@Data
public class CrmToolResponse {
private String toolName;
private String category;
private String referenceId;
private String status;
private String summary;
private String processedAt;
private List<String> details;
}

View File

@@ -1,298 +0,0 @@
package io.shinhanlife.dat.mcc.biz.crm.usecase;
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
import io.shinhanlife.dat.mcc.biz.crm.dto.CrmToolRequest;
import io.shinhanlife.dat.mcc.biz.crm.dto.CrmToolResponse;
import org.springaicommunity.mcp.annotation.McpTool;
public interface CrmToolUseCase {
@McpTool(name = "crm_customer_search", title = "고객 통합 조회", description = "고객 통합 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CUSTOMER_SEARCH",
functionDescription = "고객 통합 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 통합 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 통합 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 통합 조회 해줘", "고객 통합 조회 결과를 알려줘", "고객 통합 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse searchCustomers(CrmToolRequest request);
@McpTool(name = "crm_customer_detail", title = "고객 상세 정보 조회", description = "고객 상세 정보 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CUSTOMER_DETAIL",
functionDescription = "고객 상세 정보 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 상세 정보 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 상세 정보 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 상세 정보 조회 해줘", "고객 상세 정보 조회 결과를 알려줘", "고객 상세 정보 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse getCustomerDetail(CrmToolRequest request);
@McpTool(name = "crm_customer_create", title = "고객 정보 등록", description = "고객 정보 등록 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CUSTOMER_CREATE",
functionDescription = "고객 정보 등록 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 정보 등록 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 정보 등록 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 정보 등록 해줘", "고객 정보 등록 결과를 알려줘", "고객 정보 등록 기능을 실행해줘"},
destructive = false,
idempotent = false,
tags = {"crm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse createCustomer(CrmToolRequest request);
@McpTool(name = "crm_customer_update", title = "고객 정보 수정", description = "고객 정보 수정 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CUSTOMER_UPDATE",
functionDescription = "고객 정보 수정 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 정보 수정 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 정보 수정 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 정보 수정 해줘", "고객 정보 수정 결과를 알려줘", "고객 정보 수정 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse updateCustomer(CrmToolRequest request);
@McpTool(name = "crm_customer_duplicate_check", title = "고객 중복 여부 확인", description = "고객 중복 여부 확인 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CUSTOMER_DUPLICATE_CHECK",
functionDescription = "고객 중복 여부 확인 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 중복 여부 확인 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 중복 여부 확인 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 중복 여부 확인 해줘", "고객 중복 여부 확인 결과를 알려줘", "고객 중복 여부 확인 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse checkDuplicateCustomer(CrmToolRequest request);
@McpTool(name = "crm_consultation_history", title = "고객 상담 이력 조회", description = "고객 상담 이력 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CONSULTATION_HISTORY",
functionDescription = "고객 상담 이력 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 상담 이력 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 상담 이력 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 상담 이력 조회 해줘", "고객 상담 이력 조회 결과를 알려줘", "고객 상담 이력 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse getConsultationHistory(CrmToolRequest request);
@McpTool(name = "crm_consultation_register", title = "고객 상담 이력 등록", description = "고객 상담 이력 등록 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CONSULTATION_REGISTER",
functionDescription = "고객 상담 이력 등록 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 상담 이력 등록 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 상담 이력 등록 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 상담 이력 등록 해줘", "고객 상담 이력 등록 결과를 알려줘", "고객 상담 이력 등록 기능을 실행해줘"},
destructive = false,
idempotent = false,
tags = {"crm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse registerConsultation(CrmToolRequest request);
@McpTool(name = "crm_contact_history", title = "고객 접촉 이력 조회", description = "고객 접촉 이력 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CONTACT_HISTORY",
functionDescription = "고객 접촉 이력 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 접촉 이력 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 접촉 이력 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 접촉 이력 조회 해줘", "고객 접촉 이력 조회 결과를 알려줘", "고객 접촉 이력 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse getContactHistory(CrmToolRequest request);
@McpTool(name = "crm_contact_register", title = "고객 접촉 이력 등록", description = "고객 접촉 이력 등록 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CONTACT_REGISTER",
functionDescription = "고객 접촉 이력 등록 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 접촉 이력 등록 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 접촉 이력 등록 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 접촉 이력 등록 해줘", "고객 접촉 이력 등록 결과를 알려줘", "고객 접촉 이력 등록 기능을 실행해줘"},
destructive = false,
idempotent = false,
tags = {"crm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse registerContact(CrmToolRequest request);
@McpTool(name = "crm_grade_detail", title = "고객 등급 조회", description = "고객 등급 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_GRADE_DETAIL",
functionDescription = "고객 등급 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 등급 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 등급 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 등급 조회 해줘", "고객 등급 조회 결과를 알려줘", "고객 등급 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse getCustomerGrade(CrmToolRequest request);
@McpTool(name = "crm_grade_change", title = "고객 등급 변경", description = "고객 등급 변경 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_GRADE_CHANGE",
functionDescription = "고객 등급 변경 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 등급 변경 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 등급 변경 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 등급 변경 해줘", "고객 등급 변경 결과를 알려줘", "고객 등급 변경 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse changeCustomerGrade(CrmToolRequest request);
@McpTool(name = "crm_segment_detail", title = "고객 세그먼트 조회", description = "고객 세그먼트 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_SEGMENT_DETAIL",
functionDescription = "고객 세그먼트 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 세그먼트 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 세그먼트 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 세그먼트 조회 해줘", "고객 세그먼트 조회 결과를 알려줘", "고객 세그먼트 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse getCustomerSegment(CrmToolRequest request);
@McpTool(name = "crm_tag_manage", title = "고객 태그 관리", description = "고객 태그 관리 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_TAG_MANAGE",
functionDescription = "고객 태그 관리 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 태그 관리 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 태그 관리 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 태그 관리 해줘", "고객 태그 관리 결과를 알려줘", "고객 태그 관리 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse manageCustomerTags(CrmToolRequest request);
@McpTool(name = "crm_consent_detail", title = "고객 동의 정보 조회", description = "고객 동의 정보 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CONSENT_DETAIL",
functionDescription = "고객 동의 정보 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 동의 정보 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 동의 정보 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 동의 정보 조회 해줘", "고객 동의 정보 조회 결과를 알려줘", "고객 동의 정보 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse getCustomerConsent(CrmToolRequest request);
@McpTool(name = "crm_consent_change", title = "고객 동의 정보 변경", description = "고객 동의 정보 변경 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_CONSENT_CHANGE",
functionDescription = "고객 동의 정보 변경 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 동의 정보 변경 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 동의 정보 변경 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 동의 정보 변경 해줘", "고객 동의 정보 변경 결과를 알려줘", "고객 동의 정보 변경 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse changeCustomerConsent(CrmToolRequest request);
@McpTool(name = "crm_owner_assign", title = "고객 담당자 배정", description = "고객 담당자 배정 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_OWNER_ASSIGN",
functionDescription = "고객 담당자 배정 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 담당자 배정 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 담당자 배정 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 담당자 배정 해줘", "고객 담당자 배정 결과를 알려줘", "고객 담당자 배정 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse assignCustomerOwner(CrmToolRequest request);
@McpTool(name = "crm_activity_status", title = "고객 활동 현황 조회", description = "고객 활동 현황 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "crm",
mappingId = "DIRECT_CRM_ACTIVITY_STATUS",
functionDescription = "고객 활동 현황 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객 활동 현황 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객 활동 현황 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객 활동 현황 조회 해줘", "고객 활동 현황 조회 결과를 알려줘", "고객 활동 현황 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"crm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
CrmToolResponse getCustomerActivityStatus(CrmToolRequest request);
}

View File

@@ -1,95 +0,0 @@
package io.shinhanlife.dat.mcc.biz.crm.usecase.impl;
import io.shinhanlife.dat.mcc.biz.crm.dto.CrmToolRequest;
import io.shinhanlife.dat.mcc.biz.crm.dto.CrmToolResponse;
import io.shinhanlife.dat.mcc.biz.crm.usecase.CrmToolUseCase;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class CrmToolUseCaseImpl implements CrmToolUseCase {
@Override
public CrmToolResponse searchCustomers(CrmToolRequest request) {
return mockResponse(request, "crm_customer_search", "고객 통합 조회");
}
@Override
public CrmToolResponse getCustomerDetail(CrmToolRequest request) {
return mockResponse(request, "crm_customer_detail", "고객 상세 정보 조회");
}
@Override
public CrmToolResponse createCustomer(CrmToolRequest request) {
return mockResponse(request, "crm_customer_create", "고객 정보 등록");
}
@Override
public CrmToolResponse updateCustomer(CrmToolRequest request) {
return mockResponse(request, "crm_customer_update", "고객 정보 수정");
}
@Override
public CrmToolResponse checkDuplicateCustomer(CrmToolRequest request) {
return mockResponse(request, "crm_customer_duplicate_check", "고객 중복 여부 확인");
}
@Override
public CrmToolResponse getConsultationHistory(CrmToolRequest request) {
return mockResponse(request, "crm_consultation_history", "고객 상담 이력 조회");
}
@Override
public CrmToolResponse registerConsultation(CrmToolRequest request) {
return mockResponse(request, "crm_consultation_register", "고객 상담 이력 등록");
}
@Override
public CrmToolResponse getContactHistory(CrmToolRequest request) {
return mockResponse(request, "crm_contact_history", "고객 접촉 이력 조회");
}
@Override
public CrmToolResponse registerContact(CrmToolRequest request) {
return mockResponse(request, "crm_contact_register", "고객 접촉 이력 등록");
}
@Override
public CrmToolResponse getCustomerGrade(CrmToolRequest request) {
return mockResponse(request, "crm_grade_detail", "고객 등급 조회");
}
@Override
public CrmToolResponse changeCustomerGrade(CrmToolRequest request) {
return mockResponse(request, "crm_grade_change", "고객 등급 변경");
}
@Override
public CrmToolResponse getCustomerSegment(CrmToolRequest request) {
return mockResponse(request, "crm_segment_detail", "고객 세그먼트 조회");
}
@Override
public CrmToolResponse manageCustomerTags(CrmToolRequest request) {
return mockResponse(request, "crm_tag_manage", "고객 태그 관리");
}
@Override
public CrmToolResponse getCustomerConsent(CrmToolRequest request) {
return mockResponse(request, "crm_consent_detail", "고객 동의 정보 조회");
}
@Override
public CrmToolResponse changeCustomerConsent(CrmToolRequest request) {
return mockResponse(request, "crm_consent_change", "고객 동의 정보 변경");
}
@Override
public CrmToolResponse assignCustomerOwner(CrmToolRequest request) {
return mockResponse(request, "crm_owner_assign", "고객 담당자 배정");
}
@Override
public CrmToolResponse getCustomerActivityStatus(CrmToolRequest request) {
return mockResponse(request, "crm_activity_status", "고객 활동 현황 조회");
}
private CrmToolResponse mockResponse(CrmToolRequest request, String toolName, String title) {
CrmToolResponse response = new CrmToolResponse();
response.setToolName(toolName);
response.setCategory("CRM");
response.setReferenceId(request != null && hasText(request.getSubjectId()) ? request.getSubjectId() : "CRM-000001");
response.setStatus("정상");
response.setSummary(title + " 처리 결과");
response.setProcessedAt(LocalDateTime.now().toString());
response.setDetails(List.of(title + " 모의 상세 정보", "외부 시스템을 변경하지 않는 샘플 응답"));
return response;
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
}

View File

@@ -1,34 +0,0 @@
package io.shinhanlife.dat.mcc.biz.voc.dto;
import lombok.Data;
import org.springaicommunity.mcp.annotation.McpToolParam;
@Data
public class VocToolRequest {
@McpToolParam(description = "업무 대상 식별자", required = false)
private String subjectId;
@McpToolParam(description = "고객 식별자", required = false)
private String customerId;
@McpToolParam(description = "검색어 또는 처리 내용", required = false)
private String content;
@McpToolParam(description = "상태, 유형, 등급 또는 처리 값", required = false)
private String value;
@McpToolParam(description = "담당자 식별자", required = false)
private String assigneeId;
@McpToolParam(description = "조회 시작일(YYYY-MM-DD)", required = false)
private String startDate;
@McpToolParam(description = "조회 종료일 또는 처리 예정일(YYYY-MM-DD)", required = false)
private String endDate;
@McpToolParam(description = "페이지 번호", required = false)
private Integer page;
@McpToolParam(description = "페이지 크기", required = false)
private Integer size;
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.dat.mcc.biz.voc.dto;
import java.util.List;
import lombok.Data;
@Data
public class VocToolResponse {
private String toolName;
private String category;
private String referenceId;
private String status;
private String summary;
private String processedAt;
private List<String> details;
}

View File

@@ -1,298 +0,0 @@
package io.shinhanlife.dat.mcc.biz.voc.usecase;
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
import io.shinhanlife.dat.mcc.biz.voc.dto.VocToolRequest;
import io.shinhanlife.dat.mcc.biz.voc.dto.VocToolResponse;
import org.springaicommunity.mcp.annotation.McpTool;
public interface VocToolUseCase {
@McpTool(name = "voc_register", title = "VOC 접수 등록", description = "VOC 접수 등록 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_REGISTER",
functionDescription = "VOC 접수 등록 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 접수 등록 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 접수 등록 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 접수 등록 해줘", "VOC 접수 등록 결과를 알려줘", "VOC 접수 등록 기능을 실행해줘"},
destructive = false,
idempotent = false,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse registerVoc(VocToolRequest request);
@McpTool(name = "voc_detail", title = "VOC 상세 조회", description = "VOC 상세 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_DETAIL",
functionDescription = "VOC 상세 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 상세 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 상세 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 상세 조회 해줘", "VOC 상세 조회 결과를 알려줘", "VOC 상세 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse getVocDetail(VocToolRequest request);
@McpTool(name = "voc_search", title = "VOC 목록 검색", description = "VOC 목록 검색 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_SEARCH",
functionDescription = "VOC 목록 검색 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 목록 검색 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 목록 검색 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 목록 검색 해줘", "VOC 목록 검색 결과를 알려줘", "VOC 목록 검색 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse searchVocs(VocToolRequest request);
@McpTool(name = "voc_update", title = "VOC 내용 수정", description = "VOC 내용 수정 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_UPDATE",
functionDescription = "VOC 내용 수정 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 내용 수정 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 내용 수정 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 내용 수정 해줘", "VOC 내용 수정 결과를 알려줘", "VOC 내용 수정 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse updateVoc(VocToolRequest request);
@McpTool(name = "voc_assign", title = "VOC 담당자 배정", description = "VOC 담당자 배정 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_ASSIGN",
functionDescription = "VOC 담당자 배정 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 담당자 배정 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 담당자 배정 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 담당자 배정 해줘", "VOC 담당자 배정 결과를 알려줘", "VOC 담당자 배정 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse assignVocOwner(VocToolRequest request);
@McpTool(name = "voc_change_status", title = "VOC 처리 상태 변경", description = "VOC 처리 상태 변경 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_CHANGE_STATUS",
functionDescription = "VOC 처리 상태 변경 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 처리 상태 변경 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 처리 상태 변경 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 처리 상태 변경 해줘", "VOC 처리 상태 변경 결과를 알려줘", "VOC 처리 상태 변경 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse changeVocStatus(VocToolRequest request);
@McpTool(name = "voc_register_result", title = "VOC 처리 결과 등록", description = "VOC 처리 결과 등록 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_REGISTER_RESULT",
functionDescription = "VOC 처리 결과 등록 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 처리 결과 등록 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 처리 결과 등록 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 처리 결과 등록 해줘", "VOC 처리 결과 등록 결과를 알려줘", "VOC 처리 결과 등록 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse registerVocResult(VocToolRequest request);
@McpTool(name = "voc_send_reply", title = "VOC 답변 발송", description = "VOC 답변 발송 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_SEND_REPLY",
functionDescription = "VOC 답변 발송 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 답변 발송 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 답변 발송 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 답변 발송 해줘", "VOC 답변 발송 결과를 알려줘", "VOC 답변 발송 기능을 실행해줘"},
destructive = false,
idempotent = false,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse sendVocReply(VocToolRequest request);
@McpTool(name = "voc_transfer", title = "VOC 이관 처리", description = "VOC 이관 처리 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_TRANSFER",
functionDescription = "VOC 이관 처리 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 이관 처리 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 이관 처리 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 이관 처리 해줘", "VOC 이관 처리 결과를 알려줘", "VOC 이관 처리 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse transferVoc(VocToolRequest request);
@McpTool(name = "voc_set_priority", title = "VOC 우선순위 설정", description = "VOC 우선순위 설정 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_SET_PRIORITY",
functionDescription = "VOC 우선순위 설정 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 우선순위 설정 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 우선순위 설정 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 우선순위 설정 해줘", "VOC 우선순위 설정 결과를 알려줘", "VOC 우선순위 설정 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse setVocPriority(VocToolRequest request);
@McpTool(name = "voc_classify_type", title = "VOC 유형 분류", description = "VOC 유형 분류 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_CLASSIFY_TYPE",
functionDescription = "VOC 유형 분류 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 유형 분류 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 유형 분류 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 유형 분류 해줘", "VOC 유형 분류 결과를 알려줘", "VOC 유형 분류 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse classifyVocType(VocToolRequest request);
@McpTool(name = "voc_attachments", title = "VOC 첨부파일 조회", description = "VOC 첨부파일 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_ATTACHMENTS",
functionDescription = "VOC 첨부파일 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 첨부파일 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 첨부파일 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 첨부파일 조회 해줘", "VOC 첨부파일 조회 결과를 알려줘", "VOC 첨부파일 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse getVocAttachments(VocToolRequest request);
@McpTool(name = "voc_customer_history", title = "고객별 VOC 이력 조회", description = "고객별 VOC 이력 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_CUSTOMER_HISTORY",
functionDescription = "고객별 VOC 이력 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 고객별 VOC 이력 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "고객별 VOC 이력 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"고객별 VOC 이력 조회 해줘", "고객별 VOC 이력 조회 결과를 알려줘", "고객별 VOC 이력 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse getCustomerVocHistory(VocToolRequest request);
@McpTool(name = "voc_detect_duplicate", title = "중복 VOC 탐지", description = "중복 VOC 탐지 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_DETECT_DUPLICATE",
functionDescription = "중복 VOC 탐지 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 중복 VOC 탐지 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "중복 VOC 탐지 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"중복 VOC 탐지 해줘", "중복 VOC 탐지 결과를 알려줘", "중복 VOC 탐지 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse detectDuplicateVoc(VocToolRequest request);
@McpTool(name = "voc_urgent_list", title = "긴급 VOC 목록 조회", description = "긴급 VOC 목록 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_URGENT_LIST",
functionDescription = "긴급 VOC 목록 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 긴급 VOC 목록 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "긴급 VOC 목록 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"긴급 VOC 목록 조회 해줘", "긴급 VOC 목록 조회 결과를 알려줘", "긴급 VOC 목록 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse getUrgentVocs(VocToolRequest request);
@McpTool(name = "voc_extend_due_date", title = "VOC 처리 기한 연장", description = "VOC 처리 기한 연장 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_EXTEND_DUE_DATE",
functionDescription = "VOC 처리 기한 연장 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 처리 기한 연장 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 처리 기한 연장 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 처리 기한 연장 해줘", "VOC 처리 기한 연장 결과를 알려줘", "VOC 처리 기한 연장 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse extendVocDueDate(VocToolRequest request);
@McpTool(name = "voc_statistics", title = "VOC 통계 조회", description = "VOC 통계 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "voc",
mappingId = "DIRECT_VOC_STATISTICS",
functionDescription = "VOC 통계 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 VOC 통계 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "VOC 통계 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"VOC 통계 조회 해줘", "VOC 통계 조회 결과를 알려줘", "VOC 통계 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"voc", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
VocToolResponse getVocStatistics(VocToolRequest request);
}

View File

@@ -1,95 +0,0 @@
package io.shinhanlife.dat.mcc.biz.voc.usecase.impl;
import io.shinhanlife.dat.mcc.biz.voc.dto.VocToolRequest;
import io.shinhanlife.dat.mcc.biz.voc.dto.VocToolResponse;
import io.shinhanlife.dat.mcc.biz.voc.usecase.VocToolUseCase;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class VocToolUseCaseImpl implements VocToolUseCase {
@Override
public VocToolResponse registerVoc(VocToolRequest request) {
return mockResponse(request, "voc_register", "VOC 접수 등록");
}
@Override
public VocToolResponse getVocDetail(VocToolRequest request) {
return mockResponse(request, "voc_detail", "VOC 상세 조회");
}
@Override
public VocToolResponse searchVocs(VocToolRequest request) {
return mockResponse(request, "voc_search", "VOC 목록 검색");
}
@Override
public VocToolResponse updateVoc(VocToolRequest request) {
return mockResponse(request, "voc_update", "VOC 내용 수정");
}
@Override
public VocToolResponse assignVocOwner(VocToolRequest request) {
return mockResponse(request, "voc_assign", "VOC 담당자 배정");
}
@Override
public VocToolResponse changeVocStatus(VocToolRequest request) {
return mockResponse(request, "voc_change_status", "VOC 처리 상태 변경");
}
@Override
public VocToolResponse registerVocResult(VocToolRequest request) {
return mockResponse(request, "voc_register_result", "VOC 처리 결과 등록");
}
@Override
public VocToolResponse sendVocReply(VocToolRequest request) {
return mockResponse(request, "voc_send_reply", "VOC 답변 발송");
}
@Override
public VocToolResponse transferVoc(VocToolRequest request) {
return mockResponse(request, "voc_transfer", "VOC 이관 처리");
}
@Override
public VocToolResponse setVocPriority(VocToolRequest request) {
return mockResponse(request, "voc_set_priority", "VOC 우선순위 설정");
}
@Override
public VocToolResponse classifyVocType(VocToolRequest request) {
return mockResponse(request, "voc_classify_type", "VOC 유형 분류");
}
@Override
public VocToolResponse getVocAttachments(VocToolRequest request) {
return mockResponse(request, "voc_attachments", "VOC 첨부파일 조회");
}
@Override
public VocToolResponse getCustomerVocHistory(VocToolRequest request) {
return mockResponse(request, "voc_customer_history", "고객별 VOC 이력 조회");
}
@Override
public VocToolResponse detectDuplicateVoc(VocToolRequest request) {
return mockResponse(request, "voc_detect_duplicate", "중복 VOC 탐지");
}
@Override
public VocToolResponse getUrgentVocs(VocToolRequest request) {
return mockResponse(request, "voc_urgent_list", "긴급 VOC 목록 조회");
}
@Override
public VocToolResponse extendVocDueDate(VocToolRequest request) {
return mockResponse(request, "voc_extend_due_date", "VOC 처리 기한 연장");
}
@Override
public VocToolResponse getVocStatistics(VocToolRequest request) {
return mockResponse(request, "voc_statistics", "VOC 통계 조회");
}
private VocToolResponse mockResponse(VocToolRequest request, String toolName, String title) {
VocToolResponse response = new VocToolResponse();
response.setToolName(toolName);
response.setCategory("VOC");
response.setReferenceId(request != null && hasText(request.getSubjectId()) ? request.getSubjectId() : "VOC-000001");
response.setStatus("정상");
response.setSummary(title + " 처리 결과");
response.setProcessedAt(LocalDateTime.now().toString());
response.setDetails(List.of(title + " 모의 상세 정보", "외부 시스템을 변경하지 않는 샘플 응답"));
return response;
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
}

View File

@@ -1,34 +0,0 @@
package io.shinhanlife.dat.mcc.biz.wcm.dto;
import lombok.Data;
import org.springaicommunity.mcp.annotation.McpToolParam;
@Data
public class WcmToolRequest {
@McpToolParam(description = "업무 대상 식별자", required = false)
private String subjectId;
@McpToolParam(description = "고객 식별자", required = false)
private String customerId;
@McpToolParam(description = "검색어 또는 처리 내용", required = false)
private String content;
@McpToolParam(description = "상태, 유형, 등급 또는 처리 값", required = false)
private String value;
@McpToolParam(description = "담당자 식별자", required = false)
private String assigneeId;
@McpToolParam(description = "조회 시작일(YYYY-MM-DD)", required = false)
private String startDate;
@McpToolParam(description = "조회 종료일 또는 처리 예정일(YYYY-MM-DD)", required = false)
private String endDate;
@McpToolParam(description = "페이지 번호", required = false)
private Integer page;
@McpToolParam(description = "페이지 크기", required = false)
private Integer size;
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.dat.mcc.biz.wcm.dto;
import java.util.List;
import lombok.Data;
@Data
public class WcmToolResponse {
private String toolName;
private String category;
private String referenceId;
private String status;
private String summary;
private String processedAt;
private List<String> details;
}

View File

@@ -1,298 +0,0 @@
package io.shinhanlife.dat.mcc.biz.wcm.usecase;
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
import io.shinhanlife.dat.mcc.biz.wcm.dto.WcmToolRequest;
import io.shinhanlife.dat.mcc.biz.wcm.dto.WcmToolResponse;
import org.springaicommunity.mcp.annotation.McpTool;
public interface WcmToolUseCase {
@McpTool(name = "wcm_content_list", title = "웹 콘텐츠 목록 조회", description = "웹 콘텐츠 목록 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_LIST",
functionDescription = "웹 콘텐츠 목록 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 목록 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 목록 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 목록 조회 해줘", "웹 콘텐츠 목록 조회 결과를 알려줘", "웹 콘텐츠 목록 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse getContentList(WcmToolRequest request);
@McpTool(name = "wcm_content_detail", title = "웹 콘텐츠 상세 조회", description = "웹 콘텐츠 상세 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_DETAIL",
functionDescription = "웹 콘텐츠 상세 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 상세 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 상세 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 상세 조회 해줘", "웹 콘텐츠 상세 조회 결과를 알려줘", "웹 콘텐츠 상세 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse getContentDetail(WcmToolRequest request);
@McpTool(name = "wcm_content_create", title = "웹 콘텐츠 등록", description = "웹 콘텐츠 등록 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_CREATE",
functionDescription = "웹 콘텐츠 등록 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 등록 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 등록 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 등록 해줘", "웹 콘텐츠 등록 결과를 알려줘", "웹 콘텐츠 등록 기능을 실행해줘"},
destructive = false,
idempotent = false,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse createContent(WcmToolRequest request);
@McpTool(name = "wcm_content_update", title = "웹 콘텐츠 수정", description = "웹 콘텐츠 수정 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_UPDATE",
functionDescription = "웹 콘텐츠 수정 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 수정 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 수정 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 수정 해줘", "웹 콘텐츠 수정 결과를 알려줘", "웹 콘텐츠 수정 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse updateContent(WcmToolRequest request);
@McpTool(name = "wcm_content_delete", title = "웹 콘텐츠 삭제", description = "웹 콘텐츠 삭제 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_DELETE",
functionDescription = "웹 콘텐츠 삭제 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 삭제 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 삭제 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 삭제 해줘", "웹 콘텐츠 삭제 결과를 알려줘", "웹 콘텐츠 삭제 기능을 실행해줘"},
destructive = true,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse deleteContent(WcmToolRequest request);
@McpTool(name = "wcm_content_copy", title = "웹 콘텐츠 복사", description = "웹 콘텐츠 복사 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_COPY",
functionDescription = "웹 콘텐츠 복사 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 복사 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 복사 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 복사 해줘", "웹 콘텐츠 복사 결과를 알려줘", "웹 콘텐츠 복사 기능을 실행해줘"},
destructive = false,
idempotent = false,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse copyContent(WcmToolRequest request);
@McpTool(name = "wcm_content_preview", title = "웹 콘텐츠 미리보기", description = "웹 콘텐츠 미리보기 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_PREVIEW",
functionDescription = "웹 콘텐츠 미리보기 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 미리보기 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 미리보기 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 미리보기 해줘", "웹 콘텐츠 미리보기 결과를 알려줘", "웹 콘텐츠 미리보기 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse previewContent(WcmToolRequest request);
@McpTool(name = "wcm_content_publish", title = "웹 콘텐츠 게시", description = "웹 콘텐츠 게시 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_PUBLISH",
functionDescription = "웹 콘텐츠 게시 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 게시 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 게시 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 게시 해줘", "웹 콘텐츠 게시 결과를 알려줘", "웹 콘텐츠 게시 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse publishContent(WcmToolRequest request);
@McpTool(name = "wcm_content_unpublish", title = "웹 콘텐츠 게시 중지", description = "웹 콘텐츠 게시 중지 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_UNPUBLISH",
functionDescription = "웹 콘텐츠 게시 중지 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 게시 중지 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 게시 중지 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 게시 중지 해줘", "웹 콘텐츠 게시 중지 결과를 알려줘", "웹 콘텐츠 게시 중지 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse unpublishContent(WcmToolRequest request);
@McpTool(name = "wcm_content_schedule_publish", title = "웹 콘텐츠 예약 게시", description = "웹 콘텐츠 예약 게시 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_SCHEDULE_PUBLISH",
functionDescription = "웹 콘텐츠 예약 게시 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 예약 게시 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 예약 게시 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 예약 게시 해줘", "웹 콘텐츠 예약 게시 결과를 알려줘", "웹 콘텐츠 예약 게시 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse scheduleContentPublish(WcmToolRequest request);
@McpTool(name = "wcm_content_request_approval", title = "웹 콘텐츠 승인 요청", description = "웹 콘텐츠 승인 요청 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_REQUEST_APPROVAL",
functionDescription = "웹 콘텐츠 승인 요청 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 승인 요청 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 승인 요청 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 승인 요청 해줘", "웹 콘텐츠 승인 요청 결과를 알려줘", "웹 콘텐츠 승인 요청 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse requestContentApproval(WcmToolRequest request);
@McpTool(name = "wcm_content_review_approval", title = "웹 콘텐츠 승인·반려", description = "웹 콘텐츠 승인·반려 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_REVIEW_APPROVAL",
functionDescription = "웹 콘텐츠 승인·반려 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 승인·반려 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 승인·반려 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 승인·반려 해줘", "웹 콘텐츠 승인·반려 결과를 알려줘", "웹 콘텐츠 승인·반려 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse reviewContentApproval(WcmToolRequest request);
@McpTool(name = "wcm_content_version_history", title = "웹 콘텐츠 버전 이력 조회", description = "웹 콘텐츠 버전 이력 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_VERSION_HISTORY",
functionDescription = "웹 콘텐츠 버전 이력 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 버전 이력 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 버전 이력 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 버전 이력 조회 해줘", "웹 콘텐츠 버전 이력 조회 결과를 알려줘", "웹 콘텐츠 버전 이력 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse getContentVersionHistory(WcmToolRequest request);
@McpTool(name = "wcm_content_restore_version", title = "웹 콘텐츠 이전 버전 복원", description = "웹 콘텐츠 이전 버전 복원 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_CONTENT_RESTORE_VERSION",
functionDescription = "웹 콘텐츠 이전 버전 복원 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 웹 콘텐츠 이전 버전 복원 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "웹 콘텐츠 이전 버전 복원 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"웹 콘텐츠 이전 버전 복원 해줘", "웹 콘텐츠 이전 버전 복원 결과를 알려줘", "웹 콘텐츠 이전 버전 복원 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse restoreContentVersion(WcmToolRequest request);
@McpTool(name = "wcm_metadata_detail", title = "콘텐츠 메타데이터 조회", description = "콘텐츠 메타데이터 조회 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_METADATA_DETAIL",
functionDescription = "콘텐츠 메타데이터 조회 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 콘텐츠 메타데이터 조회 기능이 필요할 때 사용합니다.",
whenNotToUse = "정보 변경이나 승인 처리가 필요한 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 조회 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "콘텐츠 메타데이터 조회 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"콘텐츠 메타데이터 조회 해줘", "콘텐츠 메타데이터 조회 결과를 알려줘", "콘텐츠 메타데이터 조회 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "조회", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse getContentMetadata(WcmToolRequest request);
@McpTool(name = "wcm_metadata_upsert", title = "콘텐츠 메타데이터 등록·수정", description = "콘텐츠 메타데이터 등록·수정 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_METADATA_UPSERT",
functionDescription = "콘텐츠 메타데이터 등록·수정 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 콘텐츠 메타데이터 등록·수정 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "콘텐츠 메타데이터 등록·수정 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"콘텐츠 메타데이터 등록·수정 해줘", "콘텐츠 메타데이터 등록·수정 결과를 알려줘", "콘텐츠 메타데이터 등록·수정 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse upsertContentMetadata(WcmToolRequest request);
@McpTool(name = "wcm_taxonomy_manage", title = "콘텐츠 카테고리·태그 관리", description = "콘텐츠 카테고리·태그 관리 기능을 안전한 모의 데이터로 제공합니다.")
@GrowToolHint(
requiresApproval = false,
categoryKey = "wcm",
mappingId = "DIRECT_WCM_TAXONOMY_MANAGE",
functionDescription = "콘텐츠 카테고리·태그 관리 기능을 수행합니다.",
whenToUse = "고객채널 업무에서 콘텐츠 카테고리·태그 관리 기능이 필요할 때 사용합니다.",
whenNotToUse = "필수 식별자와 변경 내용이 확인되지 않은 경우에는 사용하지 않습니다.",
ioLimits = "입력 범위에 대한 모의 처리 결과만 반환하며 외부 시스템을 직접 변경하지 않습니다.",
displayDescription = "콘텐츠 카테고리·태그 관리 기능을 안전한 모의 데이터로 제공합니다.",
exampleQueries = {"콘텐츠 카테고리·태그 관리 해줘", "콘텐츠 카테고리·태그 관리 결과를 알려줘", "콘텐츠 카테고리·태그 관리 기능을 실행해줘"},
destructive = false,
idempotent = true,
tags = {"wcm", "고객채널", "처리", "mock"},
ownerOrg = "MCP_TOOL"
)
WcmToolResponse manageContentTaxonomy(WcmToolRequest request);
}

View File

@@ -1,95 +0,0 @@
package io.shinhanlife.dat.mcc.biz.wcm.usecase.impl;
import io.shinhanlife.dat.mcc.biz.wcm.dto.WcmToolRequest;
import io.shinhanlife.dat.mcc.biz.wcm.dto.WcmToolResponse;
import io.shinhanlife.dat.mcc.biz.wcm.usecase.WcmToolUseCase;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class WcmToolUseCaseImpl implements WcmToolUseCase {
@Override
public WcmToolResponse getContentList(WcmToolRequest request) {
return mockResponse(request, "wcm_content_list", "웹 콘텐츠 목록 조회");
}
@Override
public WcmToolResponse getContentDetail(WcmToolRequest request) {
return mockResponse(request, "wcm_content_detail", "웹 콘텐츠 상세 조회");
}
@Override
public WcmToolResponse createContent(WcmToolRequest request) {
return mockResponse(request, "wcm_content_create", "웹 콘텐츠 등록");
}
@Override
public WcmToolResponse updateContent(WcmToolRequest request) {
return mockResponse(request, "wcm_content_update", "웹 콘텐츠 수정");
}
@Override
public WcmToolResponse deleteContent(WcmToolRequest request) {
return mockResponse(request, "wcm_content_delete", "웹 콘텐츠 삭제");
}
@Override
public WcmToolResponse copyContent(WcmToolRequest request) {
return mockResponse(request, "wcm_content_copy", "웹 콘텐츠 복사");
}
@Override
public WcmToolResponse previewContent(WcmToolRequest request) {
return mockResponse(request, "wcm_content_preview", "웹 콘텐츠 미리보기");
}
@Override
public WcmToolResponse publishContent(WcmToolRequest request) {
return mockResponse(request, "wcm_content_publish", "웹 콘텐츠 게시");
}
@Override
public WcmToolResponse unpublishContent(WcmToolRequest request) {
return mockResponse(request, "wcm_content_unpublish", "웹 콘텐츠 게시 중지");
}
@Override
public WcmToolResponse scheduleContentPublish(WcmToolRequest request) {
return mockResponse(request, "wcm_content_schedule_publish", "웹 콘텐츠 예약 게시");
}
@Override
public WcmToolResponse requestContentApproval(WcmToolRequest request) {
return mockResponse(request, "wcm_content_request_approval", "웹 콘텐츠 승인 요청");
}
@Override
public WcmToolResponse reviewContentApproval(WcmToolRequest request) {
return mockResponse(request, "wcm_content_review_approval", "웹 콘텐츠 승인·반려");
}
@Override
public WcmToolResponse getContentVersionHistory(WcmToolRequest request) {
return mockResponse(request, "wcm_content_version_history", "웹 콘텐츠 버전 이력 조회");
}
@Override
public WcmToolResponse restoreContentVersion(WcmToolRequest request) {
return mockResponse(request, "wcm_content_restore_version", "웹 콘텐츠 이전 버전 복원");
}
@Override
public WcmToolResponse getContentMetadata(WcmToolRequest request) {
return mockResponse(request, "wcm_metadata_detail", "콘텐츠 메타데이터 조회");
}
@Override
public WcmToolResponse upsertContentMetadata(WcmToolRequest request) {
return mockResponse(request, "wcm_metadata_upsert", "콘텐츠 메타데이터 등록·수정");
}
@Override
public WcmToolResponse manageContentTaxonomy(WcmToolRequest request) {
return mockResponse(request, "wcm_taxonomy_manage", "콘텐츠 카테고리·태그 관리");
}
private WcmToolResponse mockResponse(WcmToolRequest request, String toolName, String title) {
WcmToolResponse response = new WcmToolResponse();
response.setToolName(toolName);
response.setCategory("WCMS");
response.setReferenceId(request != null && hasText(request.getSubjectId()) ? request.getSubjectId() : "WCMS-000001");
response.setStatus("정상");
response.setSummary(title + " 처리 결과");
response.setProcessedAt(LocalDateTime.now().toString());
response.setDetails(List.of(title + " 모의 상세 정보", "외부 시스템을 변경하지 않는 샘플 응답"));
return response;
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
}

View File

@@ -1,33 +0,0 @@
package io.shinhanlife.dat.mcc.cus;
/**
* @package io.shinhanlife.dat.mcc.cus
* @className DatWasCusApplication
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Import;
import io.shinhanlife.dat.lib.mcp.ToolMcpServerConfiguration;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dat.mcc", "io.shinhanlife.dat.lib"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dat.mcc", "io.shinhanlife.dat.lib"})
@EnableCaching
@Import(ToolMcpServerConfiguration.class)
public class DatWasCusApplication {
public static void main(String[] args) {
SpringApplication.run(DatWasCusApplication.class, args);
}
}

View File

@@ -1,15 +0,0 @@
# OCI 클라우드 개발 환경 전용 설정
server:
port: ${PORT:8084}
axhub:
gateway:
url: https://axhubmcp.devjun.net
spring:
config:
activate:
on-profile: dev
import:
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-dev.yml

View File

@@ -1,31 +0,0 @@
# Local 환경 전용 설정 (H2 메모리 DB 등)
spring:
config:
activate:
on-profile: local
import:
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-local.yml
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;
username: sa
password: password
h2:
console:
enabled: true
mcp:
security:
tenant-domains:
mcp-client-1: CUSTOMER,COMMON
mcp-client-2: ALL
axhub:
gateway:
url: http://localhost:8081
tool:
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
sol:
req-detail:
mock-enabled: true

View File

@@ -1,16 +0,0 @@
server:
port: ${PORT:8084}
spring:
config:
activate:
on-profile: prod
import:
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-prod.yml
axhub:
gateway:
url: ${AXHUB_GATEWAY_URL}
tool:
url: ${AXHUB_TOOL_URL}

View File

@@ -1,16 +0,0 @@
server:
port: ${PORT:8084}
spring:
config:
activate:
on-profile: test
import:
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-test.yml
axhub:
gateway:
url: ${AXHUB_GATEWAY_URL}
tool:
url: ${AXHUB_TOOL_URL}

View File

@@ -1,22 +0,0 @@
server:
port: 8084
spring:
application:
name: dat-was-cus
config:
import: optional:classpath:tool-service-manifest.yml
profiles:
active: local
logging:
level:
org.apache.kafka: ERROR
mcp:
namespace: ""
manifest:
bundle-id: was-cus
# Set the AA-assigned prefix before MCP pull activation (for example: cus.).
name-prefix: ""
security:
api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
tenant-domains:
TESTER-DEV: ALL

View File

@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 1. 로그 패턴 설정 (MDC traceId 포함) -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n" />
<!-- 2. 콘솔(Console) 출력 설정 (로컬 개발용) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 3. 파일(File) 출력 설정 (서버 운영용) -->
<!-- Logback에서 시스템 Hostname을 가져오기 위한 설정 -->
<property name="HOSTNAME" value="${HOSTNAME}" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/swlog/dat-was-cus/A01/${HOSTNAME}_A01.log</file> <!-- 현재 로그가 쌓이는 파일 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 -->
<fileNamePattern>/swlog/dat-was-cus/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.%i.log</fileNamePattern>
<!-- 개별 로그 파일 크기를 100MB로 제한하고, 전체 보관 일수는 30일, 총 로그 누적 용량은 1GB로 제한 -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 4. 기본 로깅 레벨 설정 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<!-- 5. 우리 프로젝트 패키지는 디버그 레벨까지 상세히 보기 -->
<logger name="io.shinhanlife" level="DEBUG" />
</configuration>

View File

@@ -1,4 +0,0 @@
{
"resultCode" : "SUCCESS",
"data" : "[{\"date\":\"2024-01-05\",\"message\":\"안내 내용\"}]"
}

View File

@@ -1,4 +0,0 @@
{
"resultCode" : "SUCCESS",
"claimId" : "CLM20230001"
}

View File

@@ -1,16 +0,0 @@
mcp:
manifest:
routing-functions:
- name: route_to_dat-was-cus
description-serialization: 고객 업무 서버로 요청을 라우팅합니다. 고객 정보와 고객 상담 업무를 처리합니다. 요청의 주요 엔티티와 업무 영역을 기준으로 고객 서버를 선택합니다.
server-id: dat-was-cus
category-key: cus
product-boundary: insurance
business-domain: 고객 관리
business-outcome: 고객 정보와 고객 상담 업무를 처리합니다.
primary-entities: [고객, 상담, 접촉이력]
capabilities: [고객 조회, 고객 정보 수정, 상담 이력 조회]
select-if: 고객 또는 상담 관련 요청인 경우
reject-if: 계약, 상품, 시스템 운영 업무가 주된 요청인 경우
confusable-servers: [dat-was-sal, dat-was-pro, dat-was-sys]
decision-policy: 요청의 주요 엔티티와 업무 영역을 기준으로 고객 서버를 선택합니다.

View File

@@ -1,59 +0,0 @@
plugins {
// Gateway와 모든 Tool Pod가 의존하는 공통 라이브러리 모듈입니다.
id 'java-library'
}
dependencies {
// 공통 REST Controller, HTTP Client, 예외 처리 기반입니다.
api 'org.springframework.boot:spring-boot-starter-web'
// Tool Request DTO의 Bean Validation을 지원합니다.
api 'org.springframework.boot:spring-boot-starter-validation'
// Tool Manifest/Registry 캐시 및 Redis 기반 공통 기능을 제공합니다.
api 'org.springframework.boot:spring-boot-starter-data-redis'
// Tool SLA, 로깅, 공통 Aspect를 적용합니다.
api 'org.springframework.boot:spring-boot-starter-aop'
// MCI/DB 연동에 필요한 JDBC 공통 기능입니다.
api 'org.springframework.boot:spring-boot-starter-jdbc'
// Tool 호출의 Circuit Breaker, Rate Limit, Retry 등 복원력 기능입니다.
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'
// MCI/업무 데이터 접근을 위한 MyBatis 지원입니다.
api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
// 현재 로컬 테스트 DB 및 SQL 로그 처리에 사용합니다.
api 'com.h2database:h2'
// Spring Data Redis가 사용하는 Redis Client를 공통 모듈에서 직접 참조합니다.
api 'io.lettuce:lettuce-core:6.6.0.RELEASE'
// MCI XML 전문, JSON Tool Schema/Manifest 처리에 사용합니다.
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml'
api 'com.fasterxml.jackson.core:jackson-databind'
// MCP Java SDK 2.0.0의 mcp-json-jackson2가 호출하는 Schema API와 반드시 맞춘다.
// 3.x는 Schema.validate(JsonNode) 반환 규격이 달라 런타임 NoSuchMethodError가 발생한다.
api 'com.networknt:json-schema-validator:2.0.0'
// Boot 3.5.11 위에서 MCP Java SDK 2.0.0의 Server/Client와 Servlet 전송 계층을 사용합니다.
// Jackson 2 공급자를 명시해 Boot 3의 Jackson 계열과 맞춥니다.
api 'io.modelcontextprotocol.sdk:mcp-core:2.0.0'
api 'io.modelcontextprotocol.sdk:mcp-json-jackson2:2.0.0'
// Boot 3.5 호환 Spring AI 어노테이션 API를 유지합니다.
api 'org.springaicommunity:mcp-annotations:0.9.0'
// 이벤트 기반 확장이 필요한 Tool의 공통 Kafka 연동 기능입니다.
api 'org.springframework.kafka:spring-kafka:3.2.0'
// Tool Pod REST API 문서 및 Swagger UI를 제공합니다.
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.dat.lib.adapter.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dat.lib.adapter.dto
* @className ErrorDetail
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class ErrorDetail {
private int code;
private String message;
}

View File

@@ -1,27 +0,0 @@
package io.shinhanlife.dat.lib.adapter.dto;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dat.lib.adapter.dto
* @className JsonRpcRequest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
public class JsonRpcRequest {
private String jsonrpc;
private String method;
private Params params;
private String id;
}

View File

@@ -1,30 +0,0 @@
package io.shinhanlife.dat.lib.adapter.dto;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.Setter;
// 2. 응답 DTO
/**
* @package io.shinhanlife.dat.lib.adapter.dto
* @className JsonRpcResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@JsonPropertyOrder({"jsonrpc", "result", "error", "id"})
public class JsonRpcResponse {
public String jsonrpc = "2.0";
public Object result;
public Object error;
public String id;
}

View File

@@ -1,38 +0,0 @@
package io.shinhanlife.dat.lib.adapter.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dat.lib.adapter.dto
* @className Params
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Params {
private String routingType;
private String name;
private String interfaceId;
private Map<String, Object> data;
private List<Map<String, Object>> spec;
}

View File

@@ -1,67 +0,0 @@
package io.shinhanlife.dat.lib.adapter.test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Local HTTP mock server for scaffolded HTTP Tools.
*
* <p>Each Tool Pod returns the JSON generated under
* {@code src/main/resources/mock-responses/{toolName}.json}. It is enabled only
* when {@code axhub.mock.http.enabled=true}, which the HTTP Scaffold adds to local configuration.</p>
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "axhub.mock.http", name = "enabled", havingValue = "true")
@RequestMapping("/api")
public class MockEimsHttpServer {
private final ObjectMapper objectMapper;
@PostMapping("/mock/http/{toolName:[a-z0-9_-]+}")
public ResponseEntity<JsonNode> mockToolHttpResponse(
@PathVariable String toolName,
@RequestBody(required = false) JsonNode request) {
ClassPathResource resource = new ClassPathResource("mock-responses/" + toolName + ".json");
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
try (InputStream inputStream = resource.getInputStream()) {
log.info("[MockEimsHttpServer] HTTP mock request. toolName={}, body={}", toolName, request);
return ResponseEntity.ok(objectMapper.readTree(inputStream));
} catch (JsonProcessingException e) {
log.warn("[MockEimsHttpServer] Invalid mock response JSON. toolName={}", toolName, e);
return ResponseEntity.internalServerError().build();
} catch (IOException e) {
log.warn("[MockEimsHttpServer] Unable to read mock response. toolName={}", toolName, e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/gateway")
public ResponseEntity<?> mockEimsReceiver(
@RequestHeader(value = "X-Trace-Id", required = false) String traceId,
@RequestBody Map<String, Object> request) {
String interfaceId = String.valueOf(request.getOrDefault("interfaceId", ""));
log.info("[MockEimsHttpServer] Legacy gateway mock request. traceId={}, interfaceId={}", traceId, interfaceId);
return ResponseEntity.ok(Map.of(
"status", "404",
"message", "MOCK data is not defined for interfaceId: " + interfaceId));
}
}

View File

@@ -1,35 +0,0 @@
package io.shinhanlife.dat.lib.adapter.util;
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
/**
* Logback 커스텀 컨버터
* 모든 로그 메시지(%msg)가 파일이나 콘솔에 찍히기 직전에 이 클래스를 거쳐가게 됩니다.
* 여기서 PiiMaskingUtils.mask()를 호출하여 PII(주민번호, 계좌번호 등)를 안전하게 별표(*) 처리합니다.
*/
/**
* @package io.shinhanlife.dat.lib.adapter.util
* @className PiiMaskingLogbackConverter
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class PiiMaskingLogbackConverter extends MessageConverter {
@Override
public String convert(ILoggingEvent event) {
// 원본 로그 메시지를 가져옵니다.
String originalMessage = super.convert(event);
// 정규식을 이용하여 개인정보가 포함되어 있으면 마스킹 처리하여 반환합니다.
return PiiMaskingUtils.mask(originalMessage);
}
}

View File

@@ -1,77 +0,0 @@
package io.shinhanlife.dat.lib.adapter.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @package io.shinhanlife.dat.lib.adapter.util
* @className PiiMaskingUtils
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class PiiMaskingUtils {
// 1. 주민등록번호 패턴 (ex: 900101-1234567 또는 9001011234567)
private static final Pattern RRN_PATTERN = Pattern.compile("(\\d{6})[-]?([1-4]\\d{6})");
// 2. 휴대전화번호 패턴 (ex: 010-1234-5678)
private static final Pattern PHONE_PATTERN = Pattern.compile("(01[016789])[-]?(\\d{3,4})[-]?(\\d{4})");
// 3. 신한라이프 계좌/증권번호 패턴 (단순 예시용 계좌번호 11~14자리)
private static final Pattern ACCOUNT_PATTERN = Pattern.compile("(\\d{3})-?(\\d{3})-?(\\d{5,8})");
public static String mask(String input) {
if (input == null || input.isEmpty()) {
return input;
}
String masked = input;
// [1] 주민번호 뒷자리 마스킹 (첫자리 성별 식별자는 남기고 마스킹: 900101-1******)
Matcher rrnMatcher = RRN_PATTERN.matcher(masked);
StringBuffer rrnBuffer = new StringBuffer();
while (rrnMatcher.find()) {
String firstPart = rrnMatcher.group(1);
String secondPart = rrnMatcher.group(2);
rrnMatcher.appendReplacement(rrnBuffer, firstPart + "-" + secondPart.charAt(0) + "******");
}
rrnMatcher.appendTail(rrnBuffer);
masked = rrnBuffer.toString();
// [2] 전화번호 중간자리 마스킹 (010-****-5678)
Matcher phoneMatcher = PHONE_PATTERN.matcher(masked);
StringBuffer phoneBuffer = new StringBuffer();
while (phoneMatcher.find()) {
String p1 = phoneMatcher.group(1);
String p2 = phoneMatcher.group(2);
String p3 = phoneMatcher.group(3);
String maskedP2 = p2.replaceAll(".", "*");
phoneMatcher.appendReplacement(phoneBuffer, p1 + "-" + maskedP2 + "-" + p3);
}
phoneMatcher.appendTail(phoneBuffer);
masked = phoneBuffer.toString();
// [3] 계좌번호 뒷자리 마스킹 (110-123-********)
Matcher accMatcher = ACCOUNT_PATTERN.matcher(masked);
StringBuffer accBuffer = new StringBuffer();
while (accMatcher.find()) {
String a1 = accMatcher.group(1);
String a2 = accMatcher.group(2);
String a3 = accMatcher.group(3);
String maskedA3 = a3.replaceAll(".", "*");
accMatcher.appendReplacement(accBuffer, a1 + "-" + a2 + "-" + maskedA3);
}
accMatcher.appendTail(accBuffer);
masked = accBuffer.toString();
return masked;
}
}

View File

@@ -1,47 +0,0 @@
package io.shinhanlife.dat.lib.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Spring AI @Tool 어노테이션을 보완하여 MCP 시스템 메타데이터를 추가 제공하는 힌트 어노테이션
* @package io.shinhanlife.dat.lib.annotation
* @className GrowToolHint
* @description 비즈니스 로직(Tool)과 시스템 제어 메타데이터 분리
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GrowToolHint {
boolean register() default true;
boolean requiresApproval() default false;
String categoryKey() default "com";
String mappingId() default "";
String inputSchemaResource() default "";
String outputSchemaResource() default "";
long timeoutMillis() default 5000L;
int retryMaxAttempts() default 3;
// Meta 정보 추가 (보고용 샘플)
String displayDescription() default "";
String functionDescription() default "";
String whenToUse() default "";
String whenNotToUse() default "";
String ioLimits() default "";
String[] exampleQueries() default {};
boolean destructive() default false;
boolean idempotent() default false;
String[] tags() default {};
String[] requiredEnvKeys() default {};
String ownerOrg() default "";
}

View File

@@ -1,14 +0,0 @@
package io.shinhanlife.dat.lib.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a response DTO whose generated JSON Schema must be exposed and validated for a Tool response.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface McpOutputSchema {
}

View File

@@ -1,83 +0,0 @@
package io.shinhanlife.dat.lib.aop;
/**
* @package io.shinhanlife.dat.lib.aop
* @className ToolSlaMonitoringAspect
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dat.lib.config.McpProperties;
import java.lang.reflect.Method;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class ToolSlaMonitoringAspect {
private final McpProperties mcpProperties;
// @McpTool 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다.
@Around("@annotation(org.springaicommunity.mcp.annotation.McpTool)")
public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
McpTool functionAnnotation = method.getAnnotation(McpTool.class);
// 네임스페이스 자동 주입 로직을 반영하여 최종 툴 이름을 산출합니다.
String baseName = functionAnnotation.name();
String finalName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
? mcpProperties.getNamespace() + "_" + baseName
: baseName;
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
// 실제 비즈니스 로직(툴) 실행
Object result = joinPoint.proceed();
stopWatch.stop();
long timeMillis = stopWatch.getTotalTimeMillis();
// SLA 기준을 초과하면 (예: 2초 이상) 경고 로깅 처리 가능
if (timeMillis > 2000) {
log.warn(" [SLA 경고] Tool: {} | 소요시간: {}ms | 상태: SLOW_RESPONSE", finalName, timeMillis);
} else {
log.info(" [SLA 추적] Tool: {} | 소요시간: {}ms | 상태: SUCCESS", finalName, timeMillis);
}
return result;
} catch (RuntimeException e) {
if (stopWatch.isRunning()) {
stopWatch.stop();
}
long timeMillis = stopWatch.getTotalTimeMillis();
// 에러 발생 시 명확하게 실패 로그 기록
log.error(" [SLA 장애] Tool: {} | 소요시간: {}ms | 상태: FAILED | 사유: {}", finalName, timeMillis, e.getMessage());
// 원래 흐름대로 예외를 던져서 게이트웨이나 상위 로직이 에러를 처리하게 함
throw e;
}
}
}

View File

@@ -1,29 +0,0 @@
package io.shinhanlife.dat.lib.config;
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
import java.net.http.HttpClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
/**
* Registers the temporary Glow HTTP compatibility component from the DAP library scan scope.
* The bean is only created when an official GlowHttpComponent has not already been supplied.
*/
@Configuration(proxyBeanMethods = false)
public class AxhubHttpConfiguration {
@Bean
@ConditionalOnMissingBean(GlowHttpComponent.class)
public GlowHttpComponent glowHttpComponent(RestClient.Builder restClientBuilder) {
// WireMock and legacy internal endpoints can only support HTTP/1.1.
// Avoid JDK HTTP/2 negotiation that may cause RST_STREAM responses.
HttpClient http11Client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build();
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(http11Client);
return new GlowHttpComponent(restClientBuilder.requestFactory(requestFactory));
}
}

View File

@@ -1,34 +0,0 @@
package io.shinhanlife.dat.lib.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @package io.shinhanlife.dat.lib.config
* @className CorsConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 모든 엔드포인트에 대해 CORS 허용
.allowedOriginPatterns("*") // 외부 Agent Builder 등 모든 오리진 허용
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH") // 허용할 HTTP 메서드
.allowedHeaders("*") // 모든 헤더 허용
.exposedHeaders("Mcp-Session-Id") // MCP-HTTP 세션 아이디 노출 허용
.allowCredentials(true) // 쿠키/인증 정보 허용
.maxAge(3600); // preflight 요청 캐시 시간 (초 단위)
}
}

View File

@@ -1,85 +0,0 @@
package io.shinhanlife.dat.lib.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* [Glow Framework 통신 환경 설정 클래스]
* application-glow-local.yml 의 'glow.communication' 하위 설정값들을
* 자바 객체(Bean)로 매핑하여 제공합니다.
*/
/**
* @package io.shinhanlife.dat.lib.config
* @className GlowCommunicationProperties
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "glow.communication")
public class GlowCommunicationProperties {
private Common common = new Common();
private Http http = new Http();
private Mci mci = new Mci();
private ExtMci extmci = new ExtMci();
private Eai eai = new Eai();
private Websocket websocket = new Websocket();
@Getter @Setter
public static class Common {
private String envType; // 대내표준 헤더의 환경 타입정보 (D, T, P)
}
@Getter @Setter
public static class Http {
private int connectionTimeout; // 연결 타임아웃 시간 (초 단위)
private int readTimeout; // 읽기 타임아웃 시간 (초 단위)
}
@Getter @Setter
public static class Mci {
private String host;
private int port;
private String uri;
private String receiveUri;
private int connectionTimeout;
private int readTimeout;
private String encoding;
}
@Getter @Setter
public static class ExtMci {
private String host;
private int port;
private String uri;
private String jsonUri;
private String receiveUri;
private int connectionTimeout;
private int readTimeout;
private String encoding;
}
@Getter @Setter
public static class Eai {
private String host;
private int port;
}
@Getter @Setter
public static class Websocket {
private String endpoint;
private String allowedOrigins;
}
}

View File

@@ -1,56 +0,0 @@
package io.shinhanlife.dat.lib.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import java.util.List;
/**
* @package io.shinhanlife.dat.lib.config
* @className McpProperties
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "mcp")
public class McpProperties {
private String namespace;
private Manifest manifest = new Manifest();
@Data
public static class Manifest {
private String bundleId;
private String namePrefix;
private List<RoutingFunction> routingFunctions = List.of();
}
@Data
public static class RoutingFunction {
private String name;
private String descriptionSerialization;
private String serverId;
private String categoryKey;
private String productBoundary;
private String businessDomain;
private String businessOutcome;
private List<String> primaryEntities = List.of();
private List<String> capabilities = List.of();
private String selectIf;
private String rejectIf;
private List<String> confusableServers = List.of();
private String decisionPolicy;
}
}

View File

@@ -1,37 +0,0 @@
package io.shinhanlife.dat.lib.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @package io.shinhanlife.dat.lib.config
* @className MybatisConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.mybatis.spring.annotation.MapperScan;
import io.shinhanlife.glow.GlowMybatisMapper;
@Configuration
@MapperScan(basePackages = "io.shinhanlife.dat", annotationClass = GlowMybatisMapper.class)
public class MybatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
return sessionFactory.getObject();
}
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.dat.lib.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
import io.shinhanlife.dat.lib.util.ToolSchemaResolver;
import io.shinhanlife.dat.lib.validation.ToolArgumentSchemaValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Common MCP Tool Schema Bean configuration.
*/
@Configuration
public class ToolSchemaConfiguration {
@Bean
public ToolSchemaResolver toolSchemaResolver(ObjectMapper objectMapper) {
return new ToolSchemaResolver(objectMapper);
}
@Bean
public JsonSchemaValidator mcpJsonSchemaValidator() {
return new DefaultJsonSchemaValidator();
}
@Bean
public ToolArgumentSchemaValidator toolArgumentSchemaValidator(ObjectMapper objectMapper,
JsonSchemaValidator jsonSchemaValidator) {
return new ToolArgumentSchemaValidator(objectMapper, jsonSchemaValidator);
}
}

View File

@@ -1,20 +0,0 @@
package io.shinhanlife.dat.lib.dto;
/**
* @package io.shinhanlife.dat.mcg.dto
* @className OperationType
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public enum OperationType {
READ,
WRITE
}

View File

@@ -1,46 +0,0 @@
package io.shinhanlife.dat.lib.exception;
// import io.shinhanlife.glow.core.exception.abstracts.BusinessException; (이 패키지는 현재 버전에 존재하지 않아 주석 처리함)
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dat.lib.exception
* @className BizException
* @description AX HUB 시스템 처리 Exception
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Setter
@Getter
public class BizException extends RuntimeException {
private Object[] replaceStrings;
private String code;
private String msg;
public BizException(String code) {
super("");
this.code = code;
this.replaceStrings = new Object[]{""};
}
public BizException(String code, Object[] replaceStrings) {
super("");
this.code = code;
this.replaceStrings = replaceStrings;
}
public BizException(String code, String msg) {
super("");
this.code = code;
this.msg = msg;
}
}

View File

@@ -1,105 +0,0 @@
package io.shinhanlife.dat.lib.integration;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
// TODO: 실제 Glow Framework 의존성이 추가되면 아래 주석들을 풀고 사용하세요!
// import io.shinhanlife.glow.communication.dto.CommonHeader;
// import io.shinhanlife.glow.communication.dto.Transfer;
// import io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent;
// import io.shinhanlife.glow.communication.module.mci.component.GlowExtMciComponent;
// import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent;
// import io.shinhanlife.glow.communication.util.CommonHeaderFactory;
/**
* [MCI / EAI 공통 연동 래퍼(Wrapper) 템플릿]
* 신한라이프 Glow Framework 개발표준정의서를 바탕으로 대내/대외망/EAI 통신을
* MCP 툴에서 손쉽게 호출할 수 있도록 일원화한 컴포넌트입니다.
*/
/**
* @package io.shinhanlife.dat.lib.integration
* @className GlowIntegrationCall
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Component
@RequiredArgsConstructor
public class GlowIntegrationCall {
// 1. 실제 의존성이 주입될 프레임워크 컴포넌트들 (임시 주석 처리)
/*
private final GlowMciComponent mci;
private final GlowEaiComponent eai;
private final GlowExtMciComponent extMci;
*/
/**
* 1. 대외 MCI 호출 (타행, 금융결제원 등 외부 기관)
* 가이드 2.2.2에 명시된 필수 파라미터(기관코드, 종별코드, 업무코드, 거래코드)를 모두 포함합니다.
*/
/*
public <S, R> Transfer<R> callExtMci(String itrfId, String frbuCd, String cmouDutjCd, String cmouCssfCd, String cmouTraCd, S body, Class<R> resBody) {
// 1. 공통 헤더 생성
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId);
// 2. 대외 전용 필수 코드 세팅 로직 (프레임워크 내부 스펙에 맞게 가공)
// (예: 헤더에 해당 속성들을 주입하거나 Transfer 객체에 싣는 과정 추가)
// 3. Transfer 객체 빌드
Transfer<S> req = Transfer.<S>builder()
.header(header)
.body(body)
.build();
// 4. 대외 MCI 컴포넌트를 통해 최종 전송
return extMci.call(req, resBody);
}
*/
/**
* 2. EAI 호출 (대내망 중계기)
* 가이드 2.3에 명시된 대로 수신서비스 ID 없이 인터페이스 ID(itrfId)만 필수로 받습니다.
*/
/*
public <S, R> Transfer<R> callEai(String itrfId, S body, Class<R> resBody) {
// 1. EAI는 인터페이스 ID만으로 심플하게 헤더 생성
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId);
// 2. Transfer 객체 빌드
Transfer<S> req = Transfer.<S>builder()
.header(header)
.body(body)
.build();
// 3. EAI 컴포넌트를 통해 최종 전송
return eai.call(req, resBody);
}
*/
/**
* 3. 대내 MCI 호출 (사내 시스템 간 통신)
*/
/*
public <S, R> Transfer<R> callMci(String itrfId, S body, Class<R> resBody) {
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId);
Transfer<S> req = Transfer.<S>builder()
.header(header)
.body(body)
.build();
return mci.call(req, resBody);
}
*/
}

View File

@@ -1,84 +0,0 @@
package io.shinhanlife.dat.lib.integration.dto;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
// TODO: 실제 Glow Framework 의존성이 추가되면 아래 주석을 풀고 사용하세요!
// import io.shinhanlife.glow.communication.annotation.GlowMciFieldInfo;
/**
* [대외 MCI 연동용 DTO 표준 템플릿]
* Glow Framework 개발표준정의서(2.2.1 IO 작성) 규칙을 100% 준수한 샘플입니다.
* 새로운 대외 통신 전문을 만들 때 이 파일을 복사해서 필드명과 길이만 수정하여 사용하세요.
*/
/**
* @package io.shinhanlife.dat.lib.integration.dto
* @className SampleGlowMessage
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PUBLIC) // [규칙 1] Reflection을 위한 기본 생성자 필수 (public 유지)
public class SampleGlowMessage {
// [규칙 2] @GlowMciFieldInfo 선언 필수 (order: 순서)
// @GlowMciFieldInfo(order = 1)
private MessageHeader header;
// [규칙 2] @GlowMciFieldInfo 선언 필수 (order: 순서)
// @GlowMciFieldInfo(order = 2)
private List<MessageBody> msgDtdvValu; // 다건(List) 본문 데이터
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PUBLIC)
public static class MessageHeader {
// [규칙 2] 단건 필드의 경우 length 필수 입력 (EIMS 길이와 일치해야 함)
// @GlowMciFieldInfo(order = 1, length = 1)
private String msgTnsmTypeCd;
// @GlowMciFieldInfo(order = 2, length = 8)
private int msdvLen;
// [규칙 3] 다건(List) 건수 필드의 경우, target 속성에 대상 변수명("msgDtdvValu") 필수 기입!
// @GlowMciFieldInfo(order = 3, length = 2, target = "msgDtdvValu")
private int msgRpttCc;
}
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PUBLIC)
public static class MessageBody {
// @GlowMciFieldInfo(order = 1, length = 8)
private String msgCd;
// @GlowMciFieldInfo(order = 2, length = 1)
private String msgPrnAttrCd;
// @GlowMciFieldInfo(order = 3, length = 200)
private String msgCt;
// @GlowMciFieldInfo(order = 4, length = 200)
private String anxMsgCt;
}
}

View File

@@ -1,99 +0,0 @@
package io.shinhanlife.dat.lib.integration.eai.component;
import io.shinhanlife.dat.lib.config.GlowCommunicationProperties;
import io.shinhanlife.glow.communication.dto.CommonHeader;
import io.shinhanlife.glow.communication.dto.Transfer;
import io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent;
import io.shinhanlife.glow.communication.util.CommonHeaderFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 신한라이프 내부 Glow 표준 EAI 컴포넌트 어댑터 (AXHUB)
*/
@Slf4j
@Component
@RequiredArgsConstructor
/**
* @package io.shinhanlife.dat.lib.integration.eai.component
* @className AxhubEaiComponent
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class AxhubEaiComponent {
@SuppressWarnings("rawtypes")
private final GlowEaiComponent eai;
private final GlowCommunicationProperties communicationProperties;
@SuppressWarnings("unchecked")
private <O> Transfer<O> syncEai(Transfer<Object> request) {
// LOG 저장 (AXHUB 방식 로깅)
CommonHeader reqHeader = (CommonHeader) request.getHeader();
log.info("[AxhubEaiComponent] {} EAI 호출시작 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
Transfer<O> response = (Transfer<O>) eai.sync(request);
log.info("[AxhubEaiComponent] {} EAI 호출종료 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
return response;
}
/**
* EAI 호출
*/
public <O, I> Transfer<O> call(String itrfId, String rcvSvcId, I inputDto) throws Exception {
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId, rcvSvcId);
Transfer<Object> request = Transfer.builder()
.header(header)
.body(inputDto)
.build();
return syncEai(request);
}
/**
* EAI 호출 (응답 타입 명시)
*/
@SuppressWarnings("unchecked")
public <O, I> Transfer<O> call(String itrfId, String rcvSvcId, I inputDto, Class<O> resBodyClass) throws Exception {
CommonHeader header = CommonHeaderFactory.createRequestHeader(itrfId, rcvSvcId);
Transfer<Object> request = Transfer.builder()
.header(header)
.body(inputDto)
.resBodyClass((Class<Object>) (Class<?>) resBodyClass)
.build();
return syncEai(request);
}
/**
* EAI 호출 (rcvSvcId 없는 경우)
*/
public <O, I> Transfer<O> call(String itrfId, I inputDTO, Class<O> resBodyClass) throws Exception {
String className = inputDTO.getClass().getSimpleName();
String rcvSvcId = className.replace("_I", "");
return call(itrfId, rcvSvcId, inputDTO, resBodyClass);
}
/**
* EAI 호출 (Response body class와 rcvSvcId 없는 경우)
*/
public <O, I> Transfer<O> call(String itrfId, I inputDTO) throws Exception {
String className = inputDTO.getClass().getSimpleName();
String rcvSvcId = className.replace("_I", "");
return call(itrfId, rcvSvcId, inputDTO);
}
}

View File

@@ -1,150 +0,0 @@
package io.shinhanlife.dat.lib.integration.http.component;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dat.lib.config.GlowCommunicationProperties;
import io.shinhanlife.dat.lib.mcp.McpRequestHeaderContext;
import io.shinhanlife.dat.lib.mcp.McpRequestHeaders;
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
import io.shinhanlife.glow.communication.module.http.dto.HttpBody;
import io.shinhanlife.glow.communication.module.http.dto.HttpHeader;
import io.shinhanlife.glow.communication.module.http.dto.HttpTransfer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
/**
* Tool Pod outbound HTTP component using the Glow HTTP client.
* Target URL, HTTP method, content type and Pod-to-Pod behaviour are resolved from
* {@code glow.communication.http.api-list}; business source code does not own endpoint values.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AxhubHttpComponent {
private static final String ANONYMOUS_REQUEST = "AXHUB-TOOL";
private final GlowHttpComponent http;
private final ObjectMapper json;
private final GlowCommunicationProperties communicationProperties;
private final AxhubHttpProperties properties;
/** Calls the exact URL configured for the API name. */
public <T, R> R call(String apiName, T inputDto, Class<R> responseBodyClass) {
return call(apiName, "", inputDto, responseBodyClass, 0);
}
/** Calls the configured URL with an optional resource suffix. */
public <T, R> R call(String apiName, String uri, T inputDto, Class<R> responseBodyClass) {
return call(apiName, uri, inputDto, responseBodyClass, 0);
}
public <T, R> R call(String apiName, String uri, T inputDto, Class<R> responseBodyClass, int timeout) {
AxhubHttpProperties.ApiDefinition api = resolveApi(apiName);
return execute(api, uri, inputDto, responseBodyClass, timeout);
}
/** Calls the configured URL only when the target is marked as a business Pod. */
public <T, R> R callBizPod(String apiName, T inputDto, Class<R> responseBodyClass) {
AxhubHttpProperties.ApiDefinition api = resolveApi(apiName);
if (!api.bizPod()) {
throw new IllegalArgumentException("Configured API is not a business Pod: " + apiName);
}
return execute(api, "", inputDto, responseBodyClass, 0);
}
private <T, R> R execute(AxhubHttpProperties.ApiDefinition api, String uri, T inputDto,
Class<R> responseBodyClass, int timeout) {
HttpHeader header = createHeader(api, timeout);
String requestUri = joinPath(api.url(), uri);
HttpTransfer<T> request = HttpTransfer.<T>http()
.header(header)
.domain(api.domain())
.uri(requestUri)
.method(api.method())
.contentType(contentType(api))
.responseEntity(responseBodyClass)
.body(inputDto)
.build();
log.info("[AxhubHttpComponent] Glow HTTP call. apiName={}, method={}, uri={}",
api.name(), api.method(), requestUri);
ResponseEntity<HttpBody> response = http.sync(request);
return convertResponse(response.getBody(), responseBodyClass);
}
private AxhubHttpProperties.ApiDefinition resolveApi(String apiName) {
return properties.getApiList().stream()
.filter(api -> apiName != null && apiName.equals(api.name()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No HTTP API configuration for name: " + apiName));
}
private HttpHeader createHeader(AxhubHttpProperties.ApiDefinition api, int timeout) {
HttpHeader header = new HttpHeader();
if (api.bizPod()) {
header.set("X-POD-TO-POD", "true");
}
McpRequestHeaders inbound = McpRequestHeaderContext.current();
if (inbound == null) {
header.set("X-ANONYMOUS-REQ", ANONYMOUS_REQUEST);
} else {
putIfPresent(header, "X-Guid", inbound.guid());
putIfPresent(header, "X-Praf-No", inbound.prafNo());
putIfPresent(header, "X-Request-Id", inbound.requestId());
putIfPresent(header, "X-Request-Time", inbound.requestTime());
putIfPresent(header, "X-Vrtl-Praf-No", inbound.vrtlPrafNo());
putIfPresent(header, "X-App-Code", inbound.appCode());
putIfPresent(header, "X-Project-Code", inbound.projectCode());
putIfPresent(header, "X-User-Ip", inbound.userIp());
putIfPresent(header, "X-Caller-Ip", inbound.callerIp());
putIfPresent(header, "X-Caller-Host", inbound.callerHost());
putIfPresent(header, "X-Channel", inbound.channel());
putIfPresent(header, "X-Agent-Id", inbound.agentId());
putIfPresent(header, "mcp-session-id", inbound.mcpSessionId());
}
header.setReadTimeout(timeout == 0 ? defaultReadTimeout() : timeout);
return header;
}
private int defaultReadTimeout() {
return communicationProperties == null || communicationProperties.getHttp() == null
? 0 : communicationProperties.getHttp().getReadTimeout();
}
private MediaType contentType(AxhubHttpProperties.ApiDefinition api) {
return api.contentType() == null || api.contentType().isBlank()
? MediaType.APPLICATION_JSON
: MediaType.parseMediaType(api.contentType());
}
private <R> R convertResponse(HttpBody responseBody, Class<R> responseBodyClass) {
String content = responseBody == null ? null : responseBody.content();
if (responseBodyClass == String.class) {
return responseBodyClass.cast(content);
}
try {
return json.readValue(content, responseBodyClass);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Failed to convert HTTP response to " + responseBodyClass.getSimpleName(), e);
}
}
private void putIfPresent(HttpHeader header, String name, String value) {
if (value != null && !value.isBlank()) {
header.set(name, value);
}
}
private String joinPath(String configuredUrl, String suffix) {
String left = configuredUrl == null ? "" : configuredUrl.replaceAll("/+$", "");
if (suffix == null || suffix.isBlank()) {
return left.isEmpty() ? "/" : left;
}
String right = suffix.startsWith("/") ? suffix : "/" + suffix;
return left + right;
}
}

View File

@@ -1,34 +0,0 @@
package io.shinhanlife.dat.lib.integration.http.component;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
/**
* Glow HTTP target catalog.
*
* <p>Each target follows the ShinhanLife standard: name, domain, url, method,
* content-type, and biz-pod. Target-specific values belong in application-glow*.yml.</p>
*/
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "glow.communication.http")
public class AxhubHttpProperties {
private List<ApiDefinition> apiList = new ArrayList<>();
public record ApiDefinition(
String name,
String domain,
String url,
HttpMethod method,
String contentType,
boolean bizPod
) {
}
}

View File

@@ -1,170 +0,0 @@
package io.shinhanlife.dat.lib.integration.mci.component;
import io.shinhanlife.dat.lib.session.dto.SessionDto;
import io.shinhanlife.dat.lib.util.SessionUtil;
import io.shinhanlife.dat.lib.config.GlowCommunicationProperties;
import io.shinhanlife.glow.communication.dto.CommonHeader;
import io.shinhanlife.glow.communication.dto.HeaderDefaults;
import io.shinhanlife.glow.communication.dto.Transfer;
import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent;
import io.shinhanlife.glow.communication.util.CommonHeaderFactory;
import io.shinhanlife.dat.lib.integration.mci.enums.IndvCtinRoleTyp;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 신한라이프 내부 Glow 표준 컴포넌트 어댑터 (AXHUB)
*/
@Slf4j
@Component
@RequiredArgsConstructor
/**
* @package io.shinhanlife.dat.lib.integration.mci.component
* @className AxhubMciComponent
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class AxhubMciComponent {
@SuppressWarnings("rawtypes")
private final GlowMciComponent mci;
private final GlowCommunicationProperties communicationProperties;
/** 전문생성채널유형코드 : 1 (채널계) */
private final static String TGRM_CREA_CHNN_TYPE_CD_1 = "1";
private static final String SUCO_UNBL_CODE = "NNB00147"; // 청약불가
/**
* 전문 Common Header 생성
* @param itrfName 인터페이스Id
* @param rcvSvcId 수신서비스Id
* @return Map<HeaderDefaults, String>
*/
private Map<HeaderDefaults, String> createCommonHeaderMap(String itrfName, String rcvSvcId) {
SessionDto sessionDto = SessionUtil.getSession();
Map<HeaderDefaults, String> commonHeaderMap = new HashMap<>();
commonHeaderMap.put(HeaderDefaults.ITRF_ID, itrfName);
commonHeaderMap.put(HeaderDefaults.RCV_SVC_ID, rcvSvcId);
if (sessionDto != null) {
commonHeaderMap.put(HeaderDefaults.STR_YMD, sessionDto.getStrYmd());
commonHeaderMap.put(HeaderDefaults.ACNT_OGNZ_NO, sessionDto.getBrafNo());
commonHeaderMap.put(HeaderDefaults.PSMR_ASRT_CD, sessionDto.getPsmrAsrtCd());
commonHeaderMap.put(HeaderDefaults.SBSN_RULP_ASRT_CD, sessionDto.getSbsnRulpAsrtCd());
commonHeaderMap.put(HeaderDefaults.BSDU_CD, sessionDto.getBsduCd());
commonHeaderMap.put(HeaderDefaults.BSQU_CD, sessionDto.getBsquCd());
commonHeaderMap.put(HeaderDefaults.OGNZ_ASRT_CD, sessionDto.getOgnzAsrtCd());
commonHeaderMap.put(HeaderDefaults.OGNZ_LEVE_CD, sessionDto.getOgnzLeveCd());
commonHeaderMap.put(HeaderDefaults.SCRN_ID, sessionDto.getPrgrId());
commonHeaderMap.put(HeaderDefaults.DRTM_CD, sessionDto.getPrafDutyCd());
commonHeaderMap.put(HeaderDefaults.USER_ID, sessionDto.getPrafNo());
}
commonHeaderMap.put(HeaderDefaults.INDV_CTIN_ROLE_CD, IndvCtinRoleTyp.CD_Z99.getCode());
commonHeaderMap.put(HeaderDefaults.TGRM_CREA_CHNN_TYPE_CD, TGRM_CREA_CHNN_TYPE_CD_1);
if (communicationProperties != null && communicationProperties.getCommon() != null) {
commonHeaderMap.put(HeaderDefaults.ENVR_TYPE_CD, communicationProperties.getCommon().getEnvType());
}
return commonHeaderMap;
}
@SuppressWarnings("unchecked")
private <O> Transfer<O> syncMci(Transfer<Object> request) {
// LOG 저장 (AXHUB 방식 로깅)
CommonHeader reqHeader = (CommonHeader) request.getHeader();
log.info("[AxhubMciComponent] {} MCI 호출시작 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
Transfer<O> response = (Transfer<O>) mci.sync(request);
log.info("[AxhubMciComponent] {} MCI 호출종료 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
if (response != null && response.getHeader() != null) {
CommonHeader resHeader = (CommonHeader) response.getHeader();
String tgrmDalRsltCd = resHeader.getTgrmDalRsltCd();
// TODO 추가 메시지 처리 및 오류 코드 제어 로직
}
return response;
}
public <O, I> Transfer<O> callTo(String itrfName, String rcvSvcId, I inputDto) {
Map<HeaderDefaults, String> commonHeaderMap = createCommonHeaderMap(itrfName, rcvSvcId);
CommonHeader header = CommonHeaderFactory.createRequestHeader(commonHeaderMap);
Transfer<Object> request = Transfer.builder()
.header(header)
.body(inputDto)
.build();
return syncMci(request);
}
/**
* 대내 mci 호출
* @param itrfName 인터페이스Id
* @param rcvSvcId 수신서비스Id
* @param inputDto inputDto
* @param resBodyClass resBodyClass
* @return Transfer
* @param <O> resBodyClass 제너릭
* @param <I> inputDto 제너릭
*/
@SuppressWarnings("unchecked")
public <O, I> Transfer<O> callTo(String itrfName, String rcvSvcId, I inputDto, Class<O> resBodyClass) {
Map<HeaderDefaults, String> commonHeaderMap = createCommonHeaderMap(itrfName, rcvSvcId);
CommonHeader header = CommonHeaderFactory.createRequestHeader(commonHeaderMap);
Transfer<Object> request = Transfer.builder()
.header(header)
.body(inputDto)
.resBodyClass((Class<Object>) (Class<?>) resBodyClass)
.build();
return syncMci(request);
}
/**
* 대내 mci 호출 (rcvSvcId 없는 경우)
* @param itrfName 인터페이스Id
* @param inputDTO 수신서비스Id (클래스명 대체)
* @param resBodyClass resBodyClass
* @return Transfer
* @param <O> resBodyClass 제너릭
* @param <I> inputDto 제너릭
*/
public <O, I> Transfer<O> callTo(String itrfName, I inputDTO, Class<O> resBodyClass) {
String className = inputDTO.getClass().getSimpleName();
String rcvSvcId = className.replace("_I", "");
return callTo(itrfName, rcvSvcId, inputDTO, resBodyClass);
}
/**
* 대내 mci 호출 (Response body class와 rcvSvcId 없는 경우)
* @param itrfName 인터페이스Id
* @param inputDTO 수신서비스Id (클래스명 대체)
* @return Transfer
* @param <O> resBodyClass 제너릭
* @param <I> inputDto 제너릭
*/
public <O, I> Transfer<O> callTo(String itrfName, I inputDTO) {
String className = inputDTO.getClass().getSimpleName();
String rcvSvcId = className.replace("_I", "");
return callTo(itrfName, rcvSvcId, inputDTO);
}
}

View File

@@ -1,40 +0,0 @@
package io.shinhanlife.dat.lib.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
/**
* @package io.shinhanlife.dat.lib.integration.mci.config
* @className GlowMockConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class GlowMockConfig {
@Bean
@SuppressWarnings("rawtypes")
public GlowMciComponent glowMciComponent() {
return new GlowMciComponent();
}
@Bean
@SuppressWarnings("rawtypes")
public io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent glowEaiComponent() {
return new io.shinhanlife.glow.communication.module.eai.component.GlowEaiComponent();
}
}

View File

@@ -1,40 +0,0 @@
package io.shinhanlife.dat.lib.integration.mci.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @package io.shinhanlife.dat.lib.integration.mci.config
* @className ShinhanIntegrationProperties
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "shinhan.integration")
public class ShinhanIntegrationProperties {
/**
* 환경유형코드: 운영(R), 테스트(T), 개발(D)
*/
private String envrTypeCd = "D";
private ServerInfo eai = new ServerInfo();
private ServerInfo internalMci = new ServerInfo();
@Getter
@Setter
public static class ServerInfo {
private String url;
}
}

View File

@@ -1,34 +0,0 @@
package io.shinhanlife.dat.lib.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
/**
* @package io.shinhanlife.dat.lib.integration.mci.dto
* @className MciRequestWrapper
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MciRequestWrapper<T> {
private ShinhanCommonHeaderDto tgrmCmnnhddValu;
@JsonUnwrapped
private T body;
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.dat.lib.integration.mci.dto;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dat.lib.integration.mci.dto
* @className MciResponseWrapper
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MciResponseWrapper<T> {
private ShinhanCommonHeaderDto tgrmCmnnhddValu;
@JsonUnwrapped
private T body;
}

View File

@@ -1,90 +0,0 @@
package io.shinhanlife.dat.lib.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @package io.shinhanlife.dat.lib.integration.mci.dto
* @className OlCommonHeaderDto
* @description AX HUB 시스템 처리 클래스 - OL(구 오렌지라이프) 공통 헤더
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </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

@@ -1,72 +0,0 @@
package io.shinhanlife.dat.lib.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dat.lib.integration.mci.dto
* @className ShinhanCommonHeaderDto
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </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

@@ -1,51 +0,0 @@
package io.shinhanlife.dat.lib.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dat.lib.integration.mci.dto
* @className ShinhanMessageDto
* @description AX HUB 시스템 처리 클래스 - MCI 전문 메시지부
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </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

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

View File

@@ -1,84 +0,0 @@
package io.shinhanlife.dat.lib.integration.mci.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dat.lib.integration.mci.dto
* @className SlCommonHeaderDto
* @description AX HUB 시스템 처리 클래스 - SL(신한라이프) 표준 헤더
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </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

@@ -1,27 +0,0 @@
package io.shinhanlife.dat.lib.integration.mci.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
/**
* @package io.shinhanlife.dat.lib.integration.mci.enums
* @className IndvCtinRoleTyp
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public enum IndvCtinRoleTyp {
CD_Z99("Z99");
private final String code;
}

View File

@@ -1,10 +0,0 @@
package io.shinhanlife.dat.lib.manifest;
/** Behaviour hints exposed by the Tool Service manifest. */
public record ToolManifestAnnotations(
String title,
boolean readOnlyHint,
boolean destructiveHint,
boolean idempotentHint,
boolean openWorldHint) {
}

View File

@@ -1,16 +0,0 @@
package io.shinhanlife.dat.lib.manifest;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
/** One MCP Tool declaration published by a Tool Service. */
public record ToolManifestItem(
String name,
String endpoint,
String title,
String description,
Map<String, Object> inputSchema,
Map<String, Object> outputSchema,
ToolManifestAnnotations annotations,
@JsonProperty("_meta") ToolManifestMeta meta) {
}

View File

@@ -1,20 +0,0 @@
package io.shinhanlife.dat.lib.manifest;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/** Operational metadata exposed by the Tool Service manifest. */
public record ToolManifestMeta(
String version,
long timeoutMillis,
int retryMaxAttempts,
boolean enabled,
List<String> exampleQueries,
List<String> tags,
String legacyInterfaceId,
List<String> requiredEnvKeys,
String ownerOrg,
@JsonProperty("when_to_use") String whenToUse,
@JsonProperty("when_not_to_use") String whenNotToUse,
@JsonProperty("io_limits") String ioLimits) {
}

View File

@@ -1,7 +0,0 @@
package io.shinhanlife.dat.lib.manifest;
import java.util.List;
/** Top-level response for GET /tool-manifest. */
public record ToolManifestResponse(String bundleId, String revision, List<ToolManifestItem> tools) {
}

View File

@@ -1,239 +0,0 @@
package io.shinhanlife.dat.lib.manifest;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dat.lib.config.McpProperties;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.lib.mcp.ToolRegistryHeartbeatSender;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/** Builds the Tool Service owned manifest consumed by the MCP server. */
@Service
public class ToolManifestService {
private static final long DEFAULT_TIMEOUT_MILLIS = 5000L;
private static final int DEFAULT_RETRY_MAX_ATTEMPTS = 3;
private static final AtomicLong LAST_ISSUED_REVISION = new AtomicLong();
private final Supplier<List<ToolMetadata>> toolSupplier;
private final ObjectMapper objectMapper;
private final McpProperties properties;
private String lastFingerprint;
private String lastRevision;
@Autowired
public ToolManifestService(ToolRegistryHeartbeatSender heartbeatSender, ObjectMapper objectMapper,
McpProperties properties) {
this(heartbeatSender::getAllScannedTools, objectMapper, properties);
}
ToolManifestService(Supplier<List<ToolMetadata>> toolSupplier, ObjectMapper objectMapper,
McpProperties properties) {
this.toolSupplier = toolSupplier;
this.objectMapper = objectMapper;
this.properties = properties;
}
public ToolManifestResponse currentManifest() {
return currentManifest(null);
}
public ToolManifestResponse currentManifest(String categoryKey) {
String bundleId = properties.getManifest() == null ? null : properties.getManifest().getBundleId();
if (bundleId == null || bundleId.isBlank()) {
throw new IllegalStateException("mcp.manifest.bundle-id must be configured");
}
List<ToolManifestItem> tools = toolSupplier.get().stream()
.filter(tool -> isMatchCategory(categoryKey, tool.getCategoryKey(), bundleId))
.map(this::toManifestItem)
.sorted(Comparator.comparing(ToolManifestItem::name))
.toList();
validate(tools);
return new ToolManifestResponse(bundleId, revision(bundleId, tools), tools);
}
public ToolServiceManifestResponse currentToolServiceManifest() {
McpProperties.Manifest manifest = properties.getManifest();
String serviceId = manifest == null ? null : manifest.getBundleId();
List<McpProperties.RoutingFunction> configured = manifest == null
? List.of() : defaultRoutingList(manifest.getRoutingFunctions());
List<Map<String, Object>> routingFunctions = configured.stream()
.map(this::toRoutingFunctionEnvelope)
.toList();
try {
String source = objectMapper.writeValueAsString(Map.of("serviceId", serviceId,
"routingFunctions", routingFunctions));
return new ToolServiceManifestResponse(serviceId, sha256(source), routingFunctions);
} catch (Exception exception) {
throw new IllegalStateException("Failed to build Tool Service manifest", exception);
}
}
private boolean isMatchCategory(String requestedCategory, String toolCategory, String bundleId) {
if (requestedCategory == null) {
return true;
}
if (requestedCategory.equalsIgnoreCase(toolCategory)) {
return true;
}
// Return all tools in this module if the requested category matches the module's bundle ID
if (bundleId != null &&
(bundleId.equalsIgnoreCase(requestedCategory) ||
bundleId.equalsIgnoreCase("was-" + requestedCategory))) {
return true;
}
return false;
}
private ToolManifestItem toManifestItem(ToolMetadata tool) {
String title = tool.getDisplayName() == null || tool.getDisplayName().isBlank()
? tool.getName() : tool.getDisplayName();
Map<String, Object> schema = tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema();
return new ToolManifestItem(
tool.getName(), endpoint(tool), title, tool.getDescription(), schema, tool.getOutputSchema(),
new ToolManifestAnnotations(title, isTrue(tool.getReadOnlyHint()), isTrue(tool.getDestructiveHint()),
isTrue(tool.getIdempotentHint()), isTrue(tool.getOpenWorldHint())),
new ToolManifestMeta(defaultString(tool.getSemver(), "1.0.0"),
positiveOrDefault(tool.getTimeoutMillis(), DEFAULT_TIMEOUT_MILLIS),
positiveOrDefault(tool.getRetryMaxAttempts(), DEFAULT_RETRY_MAX_ATTEMPTS),
tool.getEnabled() == null || tool.getEnabled(),
defaultList(tool.getExampleQueries()), defaultList(tool.getTags()),
tool.getMciServiceId(), defaultList(tool.getRequiredEnvKeys()), tool.getOwnerOrg(),
tool.getWhenToUse(), tool.getWhenNotToUse(), tool.getIoLimits()));
}
private void validate(List<ToolManifestItem> tools) {
String namePrefix = properties.getManifest() == null ? null : properties.getManifest().getNamePrefix();
Set<String> names = new LinkedHashSet<>();
for (ToolManifestItem tool : tools) {
if (tool.name() == null || tool.name().isBlank()) {
throw new IllegalStateException("Tool manifest contains a blank tool name");
}
if (!names.add(tool.name())) {
throw new IllegalStateException("Tool manifest contains duplicate tool name: " + tool.name());
}
if (namePrefix != null && !namePrefix.isBlank() && !tool.name().startsWith(namePrefix)) {
throw new IllegalStateException("Tool name does not match mcp.manifest.name-prefix: " + tool.name());
}
if (!"object".equals(tool.inputSchema().get("type"))) {
throw new IllegalStateException("Tool inputSchema root type must be object: " + tool.name());
}
}
}
private synchronized String revision(String bundleId, List<ToolManifestItem> tools) {
String fingerprint = fingerprint(bundleId, tools);
if (!fingerprint.equals(lastFingerprint)) {
long localMinimum = lastRevision == null ? Long.MIN_VALUE : Long.parseLong(lastRevision) + 1;
long nextTimestamp = LAST_ISSUED_REVISION.updateAndGet(previous ->
Math.max(Math.max(System.currentTimeMillis(), localMinimum), previous + 1));
lastFingerprint = fingerprint;
lastRevision = Long.toString(nextTimestamp);
}
return lastRevision;
}
private long positiveOrDefault(Long value, long defaultValue) {
return value != null && value > 0 ? value : defaultValue;
}
private int positiveOrDefault(Integer value, int defaultValue) {
return value != null && value > 0 ? value : defaultValue;
}
private String fingerprint(String bundleId, List<ToolManifestItem> tools) {
try {
return objectMapper.writeValueAsString(Map.of("bundleId", bundleId, "tools", tools));
} catch (Exception exception) {
throw new IllegalStateException("Failed to build Tool manifest revision source", exception);
}
}
private String endpoint(ToolMetadata tool) {
if (tool.getEndpoint() != null && !tool.getEndpoint().isBlank()) {
return tool.getEndpoint();
}
if (tool.getPodUrl() == null || tool.getPodUrl().isBlank()) {
return "/mcp/" + tool.getName();
}
return tool.getPodUrl().replaceAll("/+$", "") + "/mcp/" + tool.getName();
}
private Map<String, Object> emptySchema() {
return Map.of("type", "object", "properties", Map.of(), "additionalProperties", false);
}
private boolean isTrue(Boolean value) {
return Boolean.TRUE.equals(value);
}
private String defaultString(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value;
}
private List<String> defaultList(List<String> value) {
return value == null ? List.of() : List.copyOf(value);
}
private List<McpProperties.RoutingFunction> defaultRoutingList(List<McpProperties.RoutingFunction> value) {
return value == null ? List.of() : List.copyOf(value);
}
private Map<String, Object> toRoutingFunctionEnvelope(McpProperties.RoutingFunction route) {
Map<String, Object> contract = new LinkedHashMap<>();
contract.put("schema_version", "3.0");
contract.put("server_id", route.getServerId());
contract.put("category_key", route.getCategoryKey());
contract.put("product_boundary", route.getProductBoundary());
contract.put("business_domain", route.getBusinessDomain());
contract.put("business_outcome", route.getBusinessOutcome());
contract.put("primary_entities", defaultList(route.getPrimaryEntities()));
contract.put("select_if", route.getSelectIf());
contract.put("reject_if", route.getRejectIf());
contract.put("capability_index", defaultList(route.getCapabilities()));
contract.put("confusable_servers", defaultList(route.getConfusableServers()));
contract.put("decision_policy", route.getDecisionPolicy());
Map<String, Object> function = new LinkedHashMap<>();
function.put("name", route.getName());
function.put("description_serialization", routingDescription(route, contract));
function.put("routing_contract", contract);
function.put("parameters", emptySchema());
Map<String, Object> envelope = new LinkedHashMap<>();
envelope.put("type", "function");
envelope.put("function", function);
return envelope;
}
private String routingDescription(McpProperties.RoutingFunction route, Map<String, Object> contract) {
try {
return defaultString(route.getDescriptionSerialization(), String.join(" ",
nonBlank(route.getBusinessOutcome()), nonBlank(route.getDecisionPolicy())).trim());
} catch (Exception exception) {
throw new IllegalStateException("Failed to serialize routing contract", exception);
}
}
private String sha256(String value) throws Exception {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
StringBuilder result = new StringBuilder();
for (byte item : digest) result.append(String.format("%02x", item));
return result.toString();
}
private String nonBlank(String value) {
return value == null ? "" : value.trim();
}
}

View File

@@ -1,9 +0,0 @@
package io.shinhanlife.dat.lib.manifest;
import java.util.List;
import java.util.Map;
/** Server-level routing metadata loaded from tool-service-manifest.yml. */
public record ToolServiceManifestResponse(String serviceId, String revision,
List<Map<String, Object>> routingFunctions) {
}

View File

@@ -1,21 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
/** Holds optional MCP headers for the lifetime of one HTTP request thread. */
public final class McpRequestHeaderContext {
private static final ThreadLocal<McpRequestHeaders> CURRENT_HEADERS = new ThreadLocal<>();
private McpRequestHeaderContext() {
}
public static McpRequestHeaders current() {
return CURRENT_HEADERS.get();
}
static void set(McpRequestHeaders headers) {
CURRENT_HEADERS.set(headers);
}
static void clear() {
CURRENT_HEADERS.remove();
}
}

View File

@@ -1,64 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
import java.io.IOException;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import io.shinhanlife.dat.lib.session.mci.AxhubSessionService;
import io.shinhanlife.dat.lib.session.dto.SessionDto;
import org.springframework.beans.factory.annotation.Autowired;
/** Captures optional Tool request headers for an MCP HTTP call and creates the user session. */
@Component
public class McpRequestHeaderFilter implements Filter {
@Autowired(required = false)
private AxhubSessionService sessionService;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!request.getRequestURI().contains("/mcp")) {
filterChain.doFilter(request, response);
return;
}
String prafNo = request.getHeader("X-Praf-No");
McpRequestHeaderContext.set(new McpRequestHeaders(
request.getHeader("X-Guid"),
prafNo,
request.getHeader("X-Request-Id"),
request.getHeader("X-Request-Time"),
request.getHeader("X-Vrtl-Praf-No"),
request.getHeader("X-App-Code"),
request.getHeader("X-Project-Code"),
request.getHeader("X-User-Ip"),
request.getHeader("X-Caller-Ip"),
request.getHeader("X-Caller-Host"),
request.getHeader("X-Channel"),
request.getHeader("X-Agent-Id"),
request.getHeader("mcp-session-id")));
// Fetch session info via MCI using X-Praf-No (only when the service is available in a Tool Pod).
if (sessionService != null && prafNo != null && !prafNo.isEmpty()) {
SessionDto sessionDto = sessionService.fetchUserSession(prafNo);
if (sessionDto != null) {
// Set to request attribute so SessionUtil can find it
request.setAttribute("userInfo", sessionDto);
}
}
try {
filterChain.doFilter(request, response);
} finally {
McpRequestHeaderContext.clear();
}
}
}

View File

@@ -1,18 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
/** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */
public record McpRequestHeaders(
String guid,
String prafNo,
String requestId,
String requestTime,
String vrtlPrafNo,
String appCode,
String projectCode,
String userIp,
String callerIp,
String callerHost,
String channel,
String agentId,
String mcpSessionId) {
}

View File

@@ -1,122 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
import io.shinhanlife.dat.lib.util.ToolSchemaResolver;
import io.shinhanlife.dat.lib.util.ToolSchemaResolver.ToolSchemaResourceException;
import io.shinhanlife.dat.lib.validation.ToolArgumentSchemaValidator;
import io.shinhanlife.glow.BizException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/** Executes a cached Tool independently from its HTTP or MCP transport. */
@Slf4j
@Service
@RequiredArgsConstructor
public class McpToolExecutionService {
private final McpToolMethodRegistry toolMethodRegistry;
private final ObjectMapper objectMapper;
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
private final ToolSchemaResolver toolSchemaResolver;
public ToolExecutionResult execute(String functionName, McpRequestHeaders requestHeaders,
Map<String, Object> arguments) {
String requestId = requestHeaders == null ? null : requestHeaders.requestId();
String guid = requestHeaders == null ? null : requestHeaders.guid();
String mcpSessionId = requestHeaders == null ? null : requestHeaders.mcpSessionId();
log.info("[Tool] IN - X-Guid: {}, X-Request-Id: {}, tool: {}", guid, requestId, functionName);
McpToolMethodRegistry.RegisteredTool resolvedTool = toolMethodRegistry.find(functionName);
if (resolvedTool == null) {
return error(404, "TOOL_NOT_FOUND", "Tool not found: " + functionName, requestId);
}
ToolExecutionResult validationFailure = validateInput(resolvedTool, arguments, requestId);
if (validationFailure != null) {
return validationFailure;
}
try {
Object methodResult = invoke(resolvedTool, convertArgument(resolvedTool.method(), arguments));
ToolExecutionResult outputFailure = validateOutput(resolvedTool, methodResult, requestId);
if (outputFailure != null) {
return outputFailure;
}
Map<String, String> headers = new HashMap<>();
if (requestId != null) headers.put("X-Request-Id", requestId);
if (guid != null) headers.put("X-Guid", guid);
if (mcpSessionId != null) headers.put("mcp-session-id", mcpSessionId);
log.info("[Tool] OUT - X-Guid: {}, X-Request-Id: {}, tool: {}", guid, requestId, functionName);
return new ToolExecutionResult(200, methodResult, headers);
} catch (IllegalArgumentException error) {
log.error("[Tool] Tool argument conversion failed. tool={}", functionName, error);
return error(502, "TOOL_ERROR", "Tool execution failed", requestId);
} catch (IllegalAccessException error) {
log.error("[Tool] Tool method is not accessible. tool={}", functionName, error);
return error(502, "TOOL_ERROR", "Tool execution failed", requestId);
} catch (InvocationTargetException error) {
Throwable cause = error.getCause();
if (cause instanceof BizException businessError) {
log.error("[Tool] Tool business execution failed. tool={}", functionName, businessError);
return error(502, "TOOL_ERROR", "Tool execution failed", requestId);
}
if (cause instanceof RuntimeException runtimeError) {
throw runtimeError;
}
if (cause instanceof Error jvmError) {
throw jvmError;
}
log.error("[Tool] Tool execution failed. tool={}", functionName, error);
return error(502, "TOOL_ERROR", "Tool execution failed", requestId);
}
}
private ToolExecutionResult validateInput(McpToolMethodRegistry.RegisteredTool tool,
Map<String, Object> arguments, String requestId) {
if (tool.method().getParameterCount() == 0 || Map.class.isAssignableFrom(tool.method().getParameterTypes()[0])) return null;
try {
Map<String, Object> schema = toolSchemaResolver.resolve(tool.annotation(), tool.hint(), tool.method().getParameterTypes()[0]);
ValidationResponse result = toolArgumentSchemaValidator.validate(schema, arguments);
return result.valid() ? null : error(422, "INVALID_PARAM", "Tool arguments do not match the input schema", requestId);
} catch (ToolSchemaResourceException error) {
log.error("[Tool] Input schema resource loading failed. tool={}", tool.annotation().name(), error);
return null;
}
}
private Object convertArgument(Method method, Map<String, Object> arguments) {
if (method.getParameterCount() == 0 || arguments == null || Map.class.isAssignableFrom(method.getParameterTypes()[0])) return arguments;
return objectMapper.convertValue(arguments, method.getParameterTypes()[0]);
}
private Object invoke(McpToolMethodRegistry.RegisteredTool tool, Object argument)
throws IllegalAccessException, InvocationTargetException {
return tool.method().getParameterCount() == 0 ? tool.method().invoke(tool.bean()) : tool.method().invoke(tool.bean(), argument);
}
private ToolExecutionResult validateOutput(McpToolMethodRegistry.RegisteredTool tool,
Object methodResult, String requestId) {
try {
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(tool.annotation(), tool.method().getReturnType(), tool.hint());
if (!outputSchema.isEmpty() && !toolArgumentSchemaValidator.validateValue(outputSchema, methodResult).valid()) {
return error(500, "INVALID_TOOL_RESPONSE", "Tool response does not match its output schema", requestId);
}
} catch (ToolSchemaResourceException error) {
log.error("[Tool] Output schema resource loading failed. tool={}", tool.annotation().name(), error);
}
return null;
}
private ToolExecutionResult error(int statusCode, String code, String message, String requestId) {
Map<String, Object> body = new HashMap<>();
body.put("code", code);
body.put("message", message);
body.put("details", Map.of("status", Integer.toString(statusCode)));
if (requestId != null) body.put("request_id", requestId);
return new ToolExecutionResult(statusCode, body, Map.of());
}
}

View File

@@ -1,209 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
import io.shinhanlife.dat.lib.config.McpProperties;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springframework.aop.support.AopUtils;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* Caches executable {@link McpTool} methods once when a Tool Pod starts.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class McpToolMethodRegistry {
private final ApplicationContext applicationContext;
private final McpProperties mcpProperties;
private volatile Map<String, RegisteredTool> tools = Map.of();
@EventListener(ApplicationReadyEvent.class)
public void initialize() {
Map<String, RegisteredTool> discovered = new LinkedHashMap<>();
for (Object bean : applicationContext.getBeansOfType(Object.class).values()) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
for (Method declaredMethod : targetClass.getDeclaredMethods()) {
/*
* Tool Annotation 검색
*
* 1순위 : Interface
* 2순위 : 구현체
*/
ToolAnnotationMetadata metadata = findToolAnnotationMetadata(targetClass, declaredMethod);
if (metadata == null) {
continue;
}
McpTool annotation = metadata.mcpTool();
GrowToolHint hint = metadata.growToolHint();
/*
* Annotation은 Interface에서 가져올 수 있지만
* 실제 호출 Method는 구현체 Method를 사용한다.
*/
RegisteredTool tool = new RegisteredTool(bean, findInvocableMethod(bean, declaredMethod), annotation, hint);
/*
* 원래 Tool Name 등록
*/
register(discovered, annotation.name(), tool);
/*
* namespace alias 등록
*/
registerNamespaceAlias(discovered, annotation.name(), tool);
log.info("[Tool Registry] Registered Tool: {} -> {}#{}", annotation.name(), targetClass.getSimpleName(), declaredMethod.getName());
}
}
tools = Map.copyOf(discovered);
log.info("[Tool Registry] {} executable tool names cached", tools.size());
}
public RegisteredTool find(String toolName) {
return tools.get(toolName);
}
/**
* @McpTool / @GrowToolHint 검색
* <p>
* 우선순위
* <p>
* 1. Interface Method
* 2. 구현체 Method
*/
private ToolAnnotationMetadata findToolAnnotationMetadata(Class<?> targetClass, Method declaredMethod) {
/*
* ==========================================
* STEP 1.
* Interface부터 검색
* ==========================================
*/
for (Class<?> interfaceClass : ClassUtils.getAllInterfacesForClassAsSet(targetClass)) {
/*
* 구현체 Method와 동일한 Signature를 가진
* Interface Method를 찾는다.
*/
Method interfaceMethod = ReflectionUtils.findMethod(interfaceClass, declaredMethod.getName(), declaredMethod.getParameterTypes());
if (interfaceMethod == null) {
continue;
}
/*
* Interface의 @McpTool 검색
*/
McpTool mcpTool = AnnotationUtils.findAnnotation(interfaceMethod, McpTool.class);
if (mcpTool == null) {
continue;
}
/*
* @McpTool을 Interface에서 발견했다면
* @GrowToolHint 역시 같은 Interface Method에서 가져온다.
*/
GrowToolHint growToolHint = AnnotationUtils.findAnnotation(interfaceMethod, GrowToolHint.class);
log.debug("[Tool Registry] Interface @McpTool found: " + "{}#{} -> {}", interfaceClass.getSimpleName(), interfaceMethod.getName(), mcpTool.name());
return new ToolAnnotationMetadata(mcpTool, growToolHint);
}
/*
* ==========================================
* STEP 2.
* Interface에서 발견되지 않은 경우
* 구현체 Method 검색
* ==========================================
*/
McpTool mcpTool = AnnotationUtils.findAnnotation(declaredMethod, McpTool.class);
if (mcpTool == null) {
return null;
}
GrowToolHint growToolHint = AnnotationUtils.findAnnotation(declaredMethod, GrowToolHint.class);
log.debug("[Tool Registry] Implementation @McpTool found: " + "{}#{} -> {}", targetClass.getSimpleName(), declaredMethod.getName(), mcpTool.name());
return new ToolAnnotationMetadata(mcpTool, growToolHint);
}
/**
* Namespace가 존재하는 경우
* <p>
* ex)
* <p>
* pct_notice_list
* <p>
* namespace = sys
* <p>
* sys_pct_notice_list
* <p>
* 두 이름으로 동일 Tool을 조회할 수 있도록 등록
*/
private void registerNamespaceAlias(Map<String, RegisteredTool> discovered, String toolName, RegisteredTool tool) {
String namespace = mcpProperties.getNamespace();
if (StringUtils.hasText(namespace)) {
register(discovered, namespace + "_" + toolName, tool);
}
}
/**
* Tool Registry 등록
*/
private void register(Map<String, RegisteredTool> discovered, String toolName, RegisteredTool tool) {
RegisteredTool existing = discovered.putIfAbsent(toolName, tool);
if (existing != null && existing != tool) {
throw new IllegalStateException("Duplicate @McpTool name: " + toolName);
}
}
/**
* 실제 호출 가능한 Method 확보
* <p>
* Annotation 검색은 Interface에서 하더라도
* Tool 실행은 구현체 Bean을 대상으로 수행해야 한다.
*/
private Method findInvocableMethod(Object bean, Method declaredMethod) {
try {
return bean.getClass().getMethod(declaredMethod.getName(), declaredMethod.getParameterTypes());
} catch (NoSuchMethodException ignored) {
return declaredMethod;
}
}
/**
* Annotation 검색 결과
*/
private record ToolAnnotationMetadata(McpTool mcpTool, GrowToolHint growToolHint) {
}
/**
* 실제 실행 Registry 정보
*/
public record RegisteredTool(Object bean, Method method, McpTool annotation, GrowToolHint hint) {
}
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
import java.util.Map;
/** Protocol-neutral result of invoking one Tool Pod business tool. */
public record ToolExecutionResult(int statusCode, Object body, Map<String, String> headers) {
public ToolExecutionResult {
headers = headers == null ? Map.of() : Map.copyOf(headers);
}
public boolean isSuccess() {
return statusCode >= 200 && statusCode < 300;
}
}

View File

@@ -1,50 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
/** Exposes every Tool Pod through the MCP Streamable HTTP transport. */
@Slf4j
@Configuration
public class ToolMcpServerConfiguration {
@PostConstruct
public void init() {
log.warn("=================================================");
log.warn("ToolMcpServerConfiguration IS LOADED BY SPRING!!!");
log.warn("=================================================");
}
@Bean
@Primary
public HttpServletStreamableServerTransportProvider toolMcpTransportProvider() {
return HttpServletStreamableServerTransportProvider.builder()
.mcpEndpoint("/mcp")
.build();
}
/** Creates the Tool Pod MCP server directly with MCP Java SDK 2.0.0. */
@Bean(destroyMethod = "close")
public McpSyncServer toolMcpServer(HttpServletStreamableServerTransportProvider transportProvider) {
return McpServer.sync(transportProvider)
.serverInfo("dap-tool-pod", "2.0.0")
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
.build();
}
@Bean
public ServletRegistrationBean<HttpServletStreamableServerTransportProvider> toolMcpServlet(
HttpServletStreamableServerTransportProvider transportProvider) {
// "/mcp/*"로 매핑하면 BusinessToolController의 "/mcp/api/v1/tools/local" 까지 가로채게 되므로,
// 정확히 MCP 통신에 사용되는 "/mcp" 와 "/mcp/message" 두 개만 매핑합니다.
return new ServletRegistrationBean<>(transportProvider, "/mcp", "/mcp/message");
}
}

View File

@@ -1,86 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Registry 메타데이터를 MCP SDK Tool 명세로 일관되게 변환합니다. */
public final class ToolMetadataMcpMapper {
private ToolMetadataMcpMapper() {
}
public static McpSchema.Tool toTool(ToolMetadata metadata) {
McpSchema.Tool.Builder builder = McpSchema.Tool.builder()
.name(metadata.getName())
.title(defaultText(metadata.getDisplayName(), metadata.getName()))
.description(defaultText(metadata.getDescription(), metadata.getName() + " Tool"))
.inputSchema(toJsonSchema(metadata.getParametersSchema()))
.annotations(new McpSchema.ToolAnnotations(
metadata.getDisplayName(), metadata.getReadOnlyHint(), metadata.getDestructiveHint(),
metadata.getIdempotentHint(), metadata.getOpenWorldHint(), null))
.meta(meta(metadata));
if (metadata.getOutputSchema() != null && !metadata.getOutputSchema().isEmpty()) {
builder.outputSchema(metadata.getOutputSchema());
}
return builder.build();
}
public static Map<String, Object> meta(ToolMetadata metadata) {
Map<String, Object> meta = new LinkedHashMap<>();
put(meta, "version", metadata.getSemver());
put(meta, "category_key", metadata.getCategoryKey());
put(meta, "display_description", metadata.getDisplayDescription());
put(meta, "when_to_use", metadata.getWhenToUse());
put(meta, "when_not_to_use", metadata.getWhenNotToUse());
put(meta, "io_limits", metadata.getIoLimits());
put(meta, "example_queries", metadata.getExampleQueries());
put(meta, "tags", metadata.getTags());
put(meta, "legacy_interface_id", metadata.getMciServiceId());
put(meta, "required_env_keys", metadata.getRequiredEnvKeys());
put(meta, "owner_org", metadata.getOwnerOrg());
meta.put("timeoutMillis", positiveOrDefault(metadata.getTimeoutMillis(), 5000L));
meta.put("retryMaxAttempts", positiveOrDefault(metadata.getRetryMaxAttempts(), 3));
return Map.copyOf(meta);
}
private static long positiveOrDefault(Long value, long defaultValue) {
return value != null && value > 0 ? value : defaultValue;
}
private static int positiveOrDefault(Integer value, int defaultValue) {
return value != null && value > 0 ? value : defaultValue;
}
private static void put(Map<String, Object> target, String key, Object value) {
if (value instanceof String text && !text.isBlank()) {
target.put(key, text);
} else if (value instanceof List<?> list && !list.isEmpty()) {
target.put(key, List.copyOf(list));
}
}
private static String defaultText(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value;
}
@SuppressWarnings("unchecked")
private static McpSchema.JsonSchema toJsonSchema(Map<String, Object> source) {
Map<String, Object> schema = source == null ? emptySchema() : source;
return new McpSchema.JsonSchema(
String.valueOf(schema.getOrDefault("type", "object")),
schema.get("properties") instanceof Map<?, ?> properties
? (Map<String, Object>) properties : Map.of(),
schema.get("required") instanceof List<?> required ? (List<String>) required : List.of(),
schema.get("additionalProperties") instanceof Boolean additionalProperties
? additionalProperties : Boolean.TRUE,
schema.get("$defs") instanceof Map<?, ?> defs ? (Map<String, Object>) defs : Map.of(),
schema.get("definitions") instanceof Map<?, ?> definitions
? (Map<String, Object>) definitions : Map.of());
}
private static Map<String, Object> emptySchema() {
return Map.of("type", "object", "properties", Map.of(), "additionalProperties", false);
}
}

View File

@@ -1,82 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
/** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */
@Component
@ConditionalOnBean(McpToolExecutionService.class)
public class ToolPodMcpToolSynchronizer {
private final McpSyncServer mcpServer;
private final ToolRegistryHeartbeatSender heartbeatSender;
private final McpToolExecutionService toolExecutionService;
private final ObjectMapper objectMapper;
public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender,
McpToolExecutionService toolExecutionService, ObjectMapper objectMapper) {
this.mcpServer = mcpServer;
this.heartbeatSender = heartbeatSender;
this.toolExecutionService = toolExecutionService;
this.objectMapper = objectMapper;
}
@EventListener(ApplicationReadyEvent.class)
public void registerLocalTools() {
heartbeatSender.getAllScannedTools().stream()
.filter(tool -> Boolean.TRUE.equals(tool.getVisible()))
.forEach(tool -> mcpServer.addTool(specification(tool)));
}
private McpServerFeatures.SyncToolSpecification specification(ToolMetadata tool) {
McpSchema.Tool mcpTool = ToolMetadataMcpMapper.toTool(tool);
return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool)
.callHandler((context, request) -> invoke(tool.getName(), McpRequestHeaderContext.current(), request.arguments())).build();
}
private McpSchema.CallToolResult invoke(String toolName, McpRequestHeaders requestHeaders,
Map<String, Object> arguments) {
ToolExecutionResult result = toolExecutionService.execute(toolName, requestHeaders, arguments);
boolean failed = !result.isSuccess();
Object body = result.body();
try {
return McpSchema.CallToolResult.builder().addTextContent(objectMapper.writeValueAsString(body))
.structuredContent(body).isError(failed).build();
} catch (Exception error) {
return McpSchema.CallToolResult.builder().addTextContent(String.valueOf(body)).isError(failed).build();
}
}
private Map<String, Object> emptySchema() {
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
schema.put("properties", Map.of());
schema.put("additionalProperties", false);
return schema;
}
@SuppressWarnings("unchecked")
private McpSchema.JsonSchema toJsonSchema(Map<String, Object> source) {
Map<String, Object> schema = source == null ? emptySchema() : source;
return new McpSchema.JsonSchema(
String.valueOf(schema.getOrDefault("type", "object")),
schema.get("properties") instanceof Map<?, ?> properties
? (Map<String, Object>) properties : Map.of(),
schema.get("required") instanceof List<?> required
? (List<String>) required : List.of(),
schema.get("additionalProperties") instanceof Boolean additionalProperties
? additionalProperties : Boolean.TRUE,
schema.get("$defs") instanceof Map<?, ?> defs ? (Map<String, Object>) defs : Map.of(),
schema.get("definitions") instanceof Map<?, ?> definitions
? (Map<String, Object>) definitions : Map.of());
}
}

View File

@@ -1,329 +0,0 @@
package io.shinhanlife.dat.lib.mcp;
/**
* @package io.shinhanlife.dat.mcc.service
* @className ToolRegistryHeartbeatSender
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
import io.shinhanlife.dat.lib.config.McpProperties;
import io.shinhanlife.dat.lib.metadata.ToolDefinition;
import io.shinhanlife.dat.lib.metadata.ToolDefinitionRepository;
import io.shinhanlife.dat.lib.util.ToolSchemaResolver;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import jakarta.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
@Slf4j
@Component
@ConditionalOnBean(McpToolExecutionService.class)
public class ToolRegistryHeartbeatSender {
private final ApplicationContext applicationContext;
private final ObjectMapper objectMapper;
private final McpProperties mcpProperties;
private final ToolSchemaResolver toolSchemaResolver;
private final ToolDefinitionRepository toolDefinitionRepository;
@Autowired
public ToolRegistryHeartbeatSender(ApplicationContext applicationContext, ObjectMapper objectMapper, McpProperties mcpProperties, ToolSchemaResolver toolSchemaResolver, @Nullable ToolDefinitionRepository toolDefinitionRepository) {
this.applicationContext = applicationContext;
this.objectMapper = objectMapper;
this.mcpProperties = mcpProperties;
this.toolSchemaResolver = toolSchemaResolver;
this.toolDefinitionRepository = toolDefinitionRepository;
}
public ToolRegistryHeartbeatSender(ApplicationContext applicationContext, ObjectMapper objectMapper, McpProperties mcpProperties, ToolSchemaResolver toolSchemaResolver) {
this(applicationContext, objectMapper, mcpProperties, toolSchemaResolver, null);
}
@Value("${axhub.tool.url:http://localhost:8080}")
private String podUrl;
@Value("${spring.application.name:}")
private String applicationName;
@Getter
private List<ToolMetadata> allScannedTools = new ArrayList<>();
@PostConstruct
public void init() {
log.info(" [ToolScanner] 초기화 시작. Pod URL: {}", podUrl);
scanAndBuildMetadata();
}
private void scanAndBuildMetadata() {
/*
* 재호출될 가능성을 고려해서
* 기존 Metadata 제거
*/
allScannedTools.clear();
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
for (Object bean : allBeans.values()) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
for (Method method : targetClass.getDeclaredMethods()) {
/*
* Annotation 검색
*
* 1순위 : Interface
* 2순위 : 구현체
*/
ToolAnnotationMetadata annotationMetadata = findToolAnnotationMetadata(targetClass, method);
if (annotationMetadata == null) {
continue;
}
McpTool functionAnnotation = annotationMetadata.mcpTool();
GrowToolHint hintAnnotation = annotationMetadata.growToolHint();
String rawSubToolName = functionAnnotation.name();
/*
* Namespace 적용
*/
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() ? mcpProperties.getNamespace() + "_" + rawSubToolName : rawSubToolName;
/*
* register 값은 외부 Gateway 전송이 아니라
* Tool 메타데이터 호환 필드로 유지
*/
boolean isRegister = hintAnnotation == null || hintAnnotation.register();
ToolMetadata meta = new ToolMetadata();
meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString());
String displayName = functionAnnotation.title().isEmpty() ? functionAnnotation.name() : functionAnnotation.title();
meta.setDisplayName(displayName);
meta.setName(subToolName);
meta.setSemver("1.0.0");
meta.setTimeoutMillis(hintAnnotation == null ? 5000L : positiveOrDefault(hintAnnotation.timeoutMillis(), 5000L));
meta.setRetryMaxAttempts(hintAnnotation == null ? 3 : positiveOrDefault(hintAnnotation.retryMaxAttempts(), 3));
meta.setEnabled(true);
meta.setDescription(functionAnnotation.description());
meta.setCategoryKey(hintAnnotation == null || hintAnnotation.categoryKey().isBlank() ? "common" : hintAnnotation.categoryKey());
meta.setIntegrationType("REST");
meta.setMciServiceId(hintAnnotation != null ? hintAnnotation.mappingId() : "");
meta.setPodUrl(podUrl);
meta.setModuleName(applicationName);
meta.setEndpoint("/mcp/" + subToolName);
meta.setVisible(true);
meta.setIsRegistered(isRegister);
meta.setRequiresApproval(hintAnnotation != null && hintAnnotation.requiresApproval());
/*
* MCP standard hint
*/
McpTool.McpAnnotations ann = functionAnnotation.annotations();
if (ann != null) {
meta.setReadOnlyHint(ann.readOnlyHint());
meta.setDestructiveHint(ann.destructiveHint());
meta.setIdempotentHint(ann.idempotentHint());
meta.setOpenWorldHint(ann.openWorldHint());
} else {
meta.setReadOnlyHint(false);
meta.setDestructiveHint(true);
meta.setIdempotentHint(false);
meta.setOpenWorldHint(true);
}
if (hintAnnotation != null) {
if (!hintAnnotation.displayDescription().isBlank()) meta.setDisplayDescription(hintAnnotation.displayDescription());
if (!hintAnnotation.functionDescription().isBlank()) meta.setFunctionDescription(hintAnnotation.functionDescription());
if (!hintAnnotation.whenToUse().isBlank()) meta.setWhenToUse(hintAnnotation.whenToUse());
if (!hintAnnotation.whenNotToUse().isBlank()) meta.setWhenNotToUse(hintAnnotation.whenNotToUse());
if (!hintAnnotation.ioLimits().isBlank()) meta.setIoLimits(hintAnnotation.ioLimits());
if (hintAnnotation.exampleQueries().length > 0) meta.setExampleQueries(java.util.List.of(hintAnnotation.exampleQueries()));
if (hintAnnotation.tags().length > 0) meta.setTags(java.util.List.of(hintAnnotation.tags()));
if (hintAnnotation.requiredEnvKeys().length > 0) meta.setRequiredEnvKeys(java.util.List.of(hintAnnotation.requiredEnvKeys()));
if (!hintAnnotation.ownerOrg().isBlank()) meta.setOwnerOrg(hintAnnotation.ownerOrg());
}
Map<String, String> prompts = new HashMap<>();
meta.setActionPrompts(prompts);
/*
* Request / Response Schema 생성
*
* Annotation은 Interface에서 읽어오지만
* 실제 Method의 Parameter/Return Type은
* 구현체 Method를 기준으로 사용
*/
if (method.getParameterCount() > 0) {
try {
Class<?> paramType = method.getParameterTypes()[0];
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType);
meta.setParametersSchema(finalSchema);
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(functionAnnotation, method.getReturnType(), hintAnnotation);
if (!outputSchema.isEmpty()) {
meta.setOutputSchema(outputSchema);
}
} catch (Exception e) {
log.error("Failed to generate schema for {}", subToolName, e);
}
}
/*
* YML Tool Definition 병합
*/
enrichWithDefinition(meta, rawSubToolName, hintAnnotation);
allScannedTools.add(meta);
log.info(" [ToolScanner] 도구 메타데이터 생성: " + "name={}, class={}#{}, " + "isRegistered={}", subToolName, targetClass.getSimpleName(), method.getName(), isRegister);
}
}
log.info(" [ToolScanner] 총 {}개 Tool 메타데이터 생성 완료", allScannedTools.size());
}
private long positiveOrDefault(long value, long defaultValue) {
return value > 0 ? value : defaultValue;
}
private int positiveOrDefault(int value, int defaultValue) {
return value > 0 ? value : defaultValue;
}
/**
* @McpTool / @GrowToolHint 검색
* <p>
* 우선순위
* <p>
* 1. Interface
* 2. Implementation
*/
private ToolAnnotationMetadata findToolAnnotationMetadata(Class<?> targetClass, Method method) {
/*
* ========================================
* STEP 1.
* Interface부터 검색
* ========================================
*/
for (Class<?> interfaceClass : ClassUtils.getAllInterfacesForClassAsSet(targetClass)) {
Method interfaceMethod = ReflectionUtils.findMethod(interfaceClass, method.getName(), method.getParameterTypes());
if (interfaceMethod == null) {
continue;
}
McpTool mcpTool = AnnotationUtils.findAnnotation(interfaceMethod, McpTool.class);
if (mcpTool == null) {
continue;
}
GrowToolHint growToolHint = AnnotationUtils.findAnnotation(interfaceMethod, GrowToolHint.class);
log.debug(" [ToolScanner] Interface @McpTool 발견: " + "{}#{} -> {}", interfaceClass.getSimpleName(), interfaceMethod.getName(), mcpTool.name());
return new ToolAnnotationMetadata(mcpTool, growToolHint);
}
/*
* ========================================
* STEP 2.
* Interface에서 찾지 못한 경우에만
* 구현체 Method 검색
* ========================================
*/
McpTool mcpTool = AnnotationUtils.findAnnotation(method, McpTool.class);
if (mcpTool == null) {
return null;
}
GrowToolHint growToolHint = AnnotationUtils.findAnnotation(method, GrowToolHint.class);
log.debug(" [ToolScanner] Implementation @McpTool 발견: " + "{}#{} -> {}", targetClass.getSimpleName(), method.getName(), mcpTool.name());
return new ToolAnnotationMetadata(mcpTool, growToolHint);
}
private void enrichWithDefinition(ToolMetadata meta, String rawToolName, GrowToolHint hintAnnotation) {
if (toolDefinitionRepository == null) {
return;
}
toolDefinitionRepository.findByName(rawToolName).ifPresent(definition -> applyDefinition(meta, definition, hintAnnotation));
}
private void applyDefinition(ToolMetadata meta, ToolDefinition definition, GrowToolHint hintAnnotation) {
meta.setDisplayName(definition.displayName());
meta.setSemver(definition.version());
meta.setCategoryKey(definition.categoryKey());
meta.setFunctionDescription(definition.description().function());
meta.setWhenToUse(definition.description().whenToUse());
meta.setWhenNotToUse(definition.description().whenNotToUse());
meta.setIoLimits(definition.description().ioLimits());
meta.setDescription(definition.description().function());
meta.setDisplayDescription(definition.displayDescription());
meta.setExampleQueries(definition.exampleQueries());
meta.setReadOnlyHint(definition.readOnly());
meta.setDestructiveHint(definition.destructive());
meta.setIdempotentHint(definition.idempotent());
boolean explicitInputResource = hintAnnotation != null && !hintAnnotation.inputSchemaResource().isBlank();
boolean explicitOutputResource = hintAnnotation != null && !hintAnnotation.outputSchemaResource().isBlank();
if (!explicitInputResource) {
meta.setParametersSchema(definition.parametersSchema());
}
if (!explicitOutputResource && definition.outputSchema() != null && !definition.outputSchema().isEmpty()) {
meta.setOutputSchema(definition.outputSchema());
}
meta.setTags(definition.tags());
meta.setMciServiceId(definition.legacyInterfaceId());
meta.setRequiredEnvKeys(definition.requiredEnvKeys());
meta.setOwnerOrg(definition.ownerOrg());
}
/**
* Annotation 검색 결과
*/
private record ToolAnnotationMetadata(McpTool mcpTool, GrowToolHint growToolHint) {
}
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.dat.lib.mcp.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @package io.shinhanlife.dat.lib.mcp.config
* @className CacheConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Configuration
@EnableCaching
public class CacheConfig {
// 스프링이 캐시를 관리할 기본 저장소를 빈(Bean)으로 등록합니다.
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("eimsData"); // 아까 설정한 캐시 이름 등록
}
}

View File

@@ -1,41 +0,0 @@
package io.shinhanlife.dat.lib.mcp.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* @package io.shinhanlife.dat.lib.mcp.config
* @className JacksonConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Configuration
public class JacksonConfig {
// 1. JSON 변환기(ObjectMapper)를 스프링 Bean으로 등록
@Bean
@Primary
public ObjectMapper jsonMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
// 2. XML 변환기(XmlMapper)를 스프링 Bean으로 등록
@Bean
public XmlMapper xmlMapper() {
return new XmlMapper();
}
}

View File

@@ -1,58 +0,0 @@
package io.shinhanlife.dat.lib.mcp.config;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.beans.factory.annotation.Value;
import java.util.HashMap;
import java.util.Map;
/**
* @package io.shinhanlife.dat.lib.mcp.config
* @className KafkaLocalConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Configuration
public class KafkaLocalConfig {
@Value("${spring.kafka.bootstrap-servers:localhost:9092}")
private String bootstrapServers;
// 1. 카프카 전송 공장(Factory) 세팅
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
// 가짜 로컬 주소 혹은 환경변수 세팅
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
// 데이터를 카프카로 보낼 때 문자열(String) 형태로 변환하겠다는 규칙
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 3초 만에 빠른 실패 처리 (로컬 무한 대기 방지)
configProps.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 3000);
// 재접속 주기를 10초로 설정 (콘솔 로그 도배 방지)
configProps.put(ProducerConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG, 10000);
return new DefaultKafkaProducerFactory<>(configProps);
}
// 2. EaiEimsSender가 애타게 찾던 KafkaTemplate을 스프링 Bean으로 등록!
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}

View File

@@ -1,47 +0,0 @@
package io.shinhanlife.dat.lib.mcp.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @package io.shinhanlife.dat.lib.mcp.config
* @className SwaggerConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Shinhan MCP Gateway API 명세서")
.version("v1.0")
.description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter Gateway API 문서입니다."))
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8080").description("Adapter Pod (8080)"))
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("Gateway Pod (8081)"))
// DAPMS가 Tool Service 호출에 사용하는 API Key 헤더를 Swagger UI에도 추가합니다.
.addSecurityItem(new SecurityRequirement().addList("X-Tool-Server-API-Key"))
.components(new Components()
.addSecuritySchemes("X-Tool-Server-API-Key",
new SecurityScheme()
.name("X-Tool-Server-API-Key")
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.description("헤더에 API Key를 입력해주세요. ")));
}
}

View File

@@ -1,54 +0,0 @@
package io.shinhanlife.dat.lib.mcp.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import io.shinhanlife.dat.lib.mcp.security.ApiKeyInterceptor;
/**
* @package io.shinhanlife.dat.lib.mcp.config
* @className WebConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
// 1. 우리가 만든 인터셉터를 주입받습니다.
private final ApiKeyInterceptor apiKeyInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 2. 인터셉터 등록 및 검사할 URL 패턴 지정
registry.addInterceptor(apiKeyInterceptor)
.addPathPatterns("/rpc/**", "/mcp/**")
.excludePathPatterns(
"/test/**", "/health", "/error", "/mcp/api/v1/admin/**",
"/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", // Swagger UI 경로는 인증 제외
"/mcp/api/v1/tools/docs/markdown", "/favicon.ico", "/mcp/api/v1/tools/list"
);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// Swagger UI(8080)에서 Gateway(8081)로 API 호출 시 발생하는 CORS 에러 해결
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Mcp-Session-Id")
.allowCredentials(true);
}
}

View File

@@ -1,66 +0,0 @@
package io.shinhanlife.dat.lib.mcp.exception;
/**
* @package io.shinhanlife.dat.lib.mcp.exception
* @className GlobalExceptionHandler
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dat.lib.adapter.dto.ErrorDetail;
import io.shinhanlife.dat.lib.adapter.dto.JsonRpcResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@Slf4j
@RestControllerAdvice // 이 어노테이션이 전역 적용의 핵심입니다!
public class GlobalExceptionHandler {
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<Void> handleNoResourceFound(NoResourceFoundException e) {
log.warn(" [Gateway Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath());
return ResponseEntity.notFound().build();
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<JsonRpcResponse> handleIllegalArgument(IllegalArgumentException e) {
log.warn(" [Gateway Bad Request] 잘못된 요청: {}", e.getMessage());
return buildErrorResponse(-32602, "Invalid params: " + e.getMessage());
}
@ExceptionHandler(org.springframework.web.context.request.async.AsyncRequestNotUsableException.class)
public ResponseEntity<Void> handleAsyncRequestNotUsable(org.springframework.web.context.request.async.AsyncRequestNotUsableException e) {
log.debug(" [Gateway Info] 클라이언트가 비동기 연결을 종료했습니다 (정상): {}", e.getMessage());
return ResponseEntity.ok().build();
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<JsonRpcResponse> handleRuntime(RuntimeException e) {
log.error(" [Gateway Internal Error] 시스템 장애: {}", e.getMessage(), e);
return buildErrorResponse(-32603, "Internal error: " + e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<JsonRpcResponse> handleAllException(Exception e) {
log.error(" [Gateway Fatal Error] 치명적 오류 발생", e);
return buildErrorResponse(-32000, "Server error: 시스템 관리자에게 문의하세요.");
}
private ResponseEntity<JsonRpcResponse> buildErrorResponse(int code, String message) {
JsonRpcResponse response = new JsonRpcResponse();
response.setError(new ErrorDetail(code, message));
return ResponseEntity.ok(response);
}
}

View File

@@ -1,56 +0,0 @@
package io.shinhanlife.dat.lib.mcp.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
/**
* @package io.shinhanlife.dat.lib.mcp.filter
* @className MdcLoggingFilter
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Component
public class MdcLoggingFilter extends OncePerRequestFilter {
private static final String TRACE_ID_HEADER = "X-Trace-Id";
private static final String MDC_KEY = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 클라이언트가 보낸 Trace ID가 있으면 쓰고, 없으면 새로 생성
String traceId = request.getHeader(TRACE_ID_HEADER);
if (traceId == null || traceId.isEmpty()) {
// 간결하게 8자리 UUID만 사용
traceId = UUID.randomUUID().toString().substring(0, 8);
}
// 로깅 컨텍스트에 고유 ID 저장
MDC.put(MDC_KEY, traceId);
try {
// 이 요청이 처리되는 동안 찍히는 모든 log.info, log.error에 traceId가 자동으로 붙습니다.
filterChain.doFilter(request, response);
} finally {
// 메모리 누수 방지를 위해 요청이 끝나면 반드시 비워줍니다.
MDC.clear();
}
}
}

View File

@@ -1,85 +0,0 @@
package io.shinhanlife.dat.lib.mcp.security;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
/**
* @package io.shinhanlife.dat.lib.mcp.security
* @className ApiKeyInterceptor
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiKeyInterceptor implements HandlerInterceptor {
// 1. 다중 테넌트 API Key 목록이 담긴 프로퍼티 객체를 주입받습니다.
private final SecurityProperties securityProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String apiKey = request.getHeader("X-Tool-Server-API-Key");
String configuredApiKey = securityProperties.getApiKey();
Map<String, String> validApiKeys = securityProperties.getApiKeys();
boolean singleApiKeyConfigured = configuredApiKey != null && !configuredApiKey.isBlank();
boolean multipleApiKeysConfigured = validApiKeys != null && !validApiKeys.isEmpty();
// 2. 만약 프로퍼티에 API Key가 하나도 설정되어 있지 않다면 (개발/로컬 환경 등) 인증 없이 통과시킵니다.
if (!singleApiKeyConfigured && !multipleApiKeysConfigured) {
MDC.put("tenantId", "anonymous");
request.setAttribute("tenantId", "anonymous");
log.debug(" [보안 패스] 등록된 API Key 없음 - 익명 사용자(anonymous)로 통과");
return true;
}
// 3. 헤더로 들어온 API Key가 단일 공통 Key 또는 다중 테넌트 Key 목록에 존재하는지 확인합니다.
boolean matchesSingleApiKey = singleApiKeyConfigured && configuredApiKey.equals(apiKey);
boolean matchesMultipleApiKeys = apiKey != null && multipleApiKeysConfigured && validApiKeys.containsKey(apiKey);
if (!matchesSingleApiKey && !matchesMultipleApiKeys) {
log.warn(" [보안 차단] 유효하지 않은 API Key 접근 시도 - IP: {}", request.getRemoteAddr());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API Key");
return false; // 컨트롤러로 넘어가지 않음
}
// 4. 유효하다면 해당 키에 맵핑된 Tenant ID(식별자)를 가져옵니다. (ex. mcp-client-1)
String tenantId = matchesSingleApiKey ? "dapms" : validApiKeys.get(apiKey);
// 4. 추출한 Tenant ID를 현재 스레드의 로깅 컨텍스트(MDC)에 저장합니다.
// 이렇게 하면 이 요청이 끝날 때까지 찍히는 모든 로그에 어떤 테넌트가 호출했는지 자동으로 기록됩니다.
MDC.put("tenantId", tenantId);
// 5. 필요시 컨트롤러 로직에서 사용할 수 있도록 Request 속성에도 담아줍니다.
request.setAttribute("tenantId", tenantId);
log.debug(" [보안 통과] API Key 인증 성공 - 접속 테넌트: {}", tenantId);
return true; // 인증 통과! 컨트롤러로 진행
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 6. 메모리 누수를 방지하기 위해 요청 처리가 완전히 끝나면 MDC에서 테넌트 정보를 지워줍니다.
MDC.remove("tenantId");
}
}

View File

@@ -1,43 +0,0 @@
package io.shinhanlife.dat.lib.mcp.security;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* [다중 테넌트 설정 매핑 클래스]
* application-local.properties 파일에 정의된 mcp.security.api-keys.* 설정들을
* Map 자료구조로 자동 바인딩(주입) 받기 위한 설정 클래스입니다.
*/
/**
* @package io.shinhanlife.dat.lib.mcp.security
* @className SecurityProperties
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Data
@Component
@ConfigurationProperties(prefix = "mcp.security")
public class SecurityProperties {
// DAPMS가 X-Tool-Server-API-Key 헤더로 전달하는 단일 공통 Key
private String apiKey;
// API Key를 Key로, Tenant ID를 Value로 가지는 맵
private Map<String, String> apiKeys = new HashMap<>();
// Tenant ID를 Key로, 허용된 도메인 그룹 목록을 Value로 가지는 맵 (ex. mcp-client-1 -> [CUSTOMER, COMMON])
// 만약 "ALL" 이 포함되어 있다면 모든 도메인에 접근 허용
private Map<String, List<String>> tenantDomains = new HashMap<>();
}

View File

@@ -1,37 +0,0 @@
package io.shinhanlife.dat.lib.metadata;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
/** BC-DAB-STD-003 V17 Tool 정의 파일의 불변 모델입니다. */
public record ToolDefinition(
String name,
@JsonProperty("display_name") String displayName,
String version,
@JsonProperty("category_key") String categoryKey,
ToolDescription description,
@JsonProperty("display_description") String displayDescription,
@JsonProperty("example_queries") List<String> exampleQueries,
@JsonProperty("read_only") Boolean readOnly,
Boolean destructive,
Boolean idempotent,
@JsonProperty("parameters_schema") Map<String, Object> parametersSchema,
@JsonProperty("output_schema") Map<String, Object> outputSchema,
List<String> tags,
@JsonProperty("legacy_interface_id") String legacyInterfaceId,
@JsonProperty("required_env_keys") List<String> requiredEnvKeys,
@JsonProperty("owner_org") String ownerOrg) {
public ToolDefinition withExampleQueries(List<String> queries) {
return new ToolDefinition(name, displayName, version, categoryKey, description, displayDescription,
queries, readOnly, destructive, idempotent, parametersSchema, outputSchema, tags, legacyInterfaceId,
requiredEnvKeys, ownerOrg);
}
public ToolDefinition withName(String value) {
return new ToolDefinition(value, displayName, version, categoryKey, description, displayDescription,
exampleQueries, readOnly, destructive, idempotent, parametersSchema, outputSchema, tags, legacyInterfaceId,
requiredEnvKeys, ownerOrg);
}
}

View File

@@ -1,62 +0,0 @@
package io.shinhanlife.dat.lib.metadata;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Component;
/** classpath의 Tool 정의를 기동 시 한 번 읽어 name 기준으로 캐시합니다. */
@Component
public class ToolDefinitionRepository {
public static final String DEFAULT_LOCATION = "classpath*:tool-definitions/**/*.yml";
private final Map<String, ToolDefinition> definitions;
@Autowired
public ToolDefinitionRepository(ResourceLoader resourceLoader) {
this(new ObjectMapper(new YAMLFactory()), resourceLoader, DEFAULT_LOCATION);
}
public ToolDefinitionRepository(ObjectMapper yamlMapper, ResourceLoader resourceLoader, String location) {
this(yamlMapper, new PathMatchingResourcePatternResolver(resourceLoader), location);
}
private ToolDefinitionRepository(ObjectMapper yamlMapper, ResourcePatternResolver resolver, String location) {
this.definitions = Collections.unmodifiableMap(load(yamlMapper, resolver, location));
}
public Optional<ToolDefinition> findByName(String name) {
return Optional.ofNullable(definitions.get(name));
}
public Map<String, ToolDefinition> findAll() {
return definitions;
}
private Map<String, ToolDefinition> load(ObjectMapper mapper, ResourcePatternResolver resolver, String location) {
Map<String, ToolDefinition> loaded = new LinkedHashMap<>();
try {
for (Resource resource : resolver.getResources(location)) {
ToolDefinition definition = mapper.readValue(resource.getInputStream(), ToolDefinition.class);
String source = resource.getDescription();
ToolDefinitionValidator.validate(definition, source);
ToolDefinition previous = loaded.putIfAbsent(definition.name(), definition);
if (previous != null) {
throw new IllegalStateException("Duplicate Tool definition name: " + definition.name());
}
}
return loaded;
} catch (IOException exception) {
throw new IllegalStateException("Failed to load Tool definitions from " + location, exception);
}
}
}

View File

@@ -1,84 +0,0 @@
package io.shinhanlife.dat.lib.metadata;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/** Tool 정의가 BC-DAB-STD-003 V17 필수 규칙을 만족하는지 검증합니다. */
public final class ToolDefinitionValidator {
private static final Pattern TOOL_NAME = Pattern.compile("^[a-z][a-z0-9_]{2,63}$");
private ToolDefinitionValidator() {
}
public static void validate(ToolDefinition definition, String source) {
if (definition == null) {
fail(source, "definition", "문서가 비어 있습니다");
}
if (isBlank(definition.name()) || !TOOL_NAME.matcher(definition.name()).matches()) {
fail(source, "name", "^[a-z][a-z0-9_]{2,63}$ 형식이어야 합니다");
}
requireText(source, "display_name", definition.displayName());
requireText(source, "version", definition.version());
requireText(source, "category_key", definition.categoryKey());
requireText(source, "display_description", definition.displayDescription());
if (definition.description() == null) {
fail(source, "description", "필수입니다");
}
requireText(source, "description.function", definition.description().function());
requireText(source, "description.when_to_use", definition.description().whenToUse());
requireText(source, "description.when_not_to_use", definition.description().whenNotToUse());
requireText(source, "description.io_limits", definition.description().ioLimits());
List<String> examples = definition.exampleQueries();
if (examples == null || examples.size() < 3 || examples.size() > 10
|| examples.stream().anyMatch(ToolDefinitionValidator::isBlank)) {
fail(source, "example_queries", "비어 있지 않은 자연어 질의가 3~10개 필요합니다");
}
if (examples.stream().anyMatch(query -> query.contains(definition.name()))) {
fail(source, "example_queries", "Tool name을 직접 포함할 수 없습니다");
}
if (definition.readOnly() == null || definition.destructive() == null || definition.idempotent() == null) {
fail(source, "annotations", "read_only, destructive, idempotent는 필수입니다");
}
validateSchema(definition.parametersSchema(), source);
if (definition.outputSchema() != null && !definition.outputSchema().isEmpty()) {
validateSchema(definition.outputSchema(), source + " output_schema");
}
}
@SuppressWarnings("unchecked")
private static void validateSchema(Map<String, Object> schema, String source) {
if (schema == null || !"object".equals(schema.get("type"))) {
fail(source, "parameters_schema.type", "object여야 합니다");
}
if (!Boolean.FALSE.equals(schema.get("additionalProperties"))) {
fail(source, "parameters_schema.additionalProperties", "false여야 합니다");
}
Object propertiesValue = schema.get("properties");
if (!(propertiesValue instanceof Map<?, ?>)) {
fail(source, "parameters_schema.properties", "object여야 합니다");
}
Map<?, ?> properties = (Map<?, ?>) propertiesValue;
for (Map.Entry<?, ?> entry : properties.entrySet()) {
if (!(entry.getValue() instanceof Map<?, ?> property)
|| isBlank(String.valueOf(property.containsKey("description")
? property.get("description") : ""))) {
fail(source, "parameters_schema.properties." + entry.getKey() + ".description", "필수입니다");
}
}
}
private static void requireText(String source, String field, String value) {
if (isBlank(value)) {
fail(source, field, "필수입니다");
}
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
private static void fail(String source, String field, String message) {
throw new IllegalStateException("Invalid Tool definition [" + source + "] " + field + ": " + message);
}
}

View File

@@ -1,11 +0,0 @@
package io.shinhanlife.dat.lib.metadata;
import com.fasterxml.jackson.annotation.JsonProperty;
/** LLM이 Tool 선택 여부를 판단할 때 사용하는 V17 목적 설명입니다. */
public record ToolDescription(
String function,
@JsonProperty("when_to_use") String whenToUse,
@JsonProperty("when_not_to_use") String whenNotToUse,
@JsonProperty("io_limits") String ioLimits) {
}

View File

@@ -1,69 +0,0 @@
package io.shinhanlife.dat.lib.paging;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
/**
* @package io.shinhanlife.dat.lib.paging
* @className MciPagingUtils
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Slf4j
public final class MciPagingUtils {
private MciPagingUtils() {
}
/**
* 페이징 응답 데이터(List 필드)를 대상(target) 객체에 순차적으로 병합(append)합니다.
* 클래스 상속 계층 구조를 탐색하여 모든 List 타입 필드를 병합합니다.
*
* @param target 누적할 최종 응답 DTO
* @param src 각 페이지별 응답 DTO
*/
public static void mergeResponseData(Object target, Object src) {
if (target == null || src == null) {
return;
}
Class<?> current = target.getClass();
while (current != null && current != Object.class) {
for (Field field : current.getDeclaredFields()) {
if (List.class.isAssignableFrom(field.getType())) {
field.setAccessible(true);
try {
List<?> srcList = (List<?>) field.get(src);
if (srcList != null && !srcList.isEmpty()) {
@SuppressWarnings("unchecked")
List<Object> targetList = (List<Object>) field.get(target);
if (targetList == null) {
targetList = new ArrayList<>(srcList);
field.set(target, targetList);
} else {
if (!(targetList instanceof ArrayList)) {
targetList = new ArrayList<>(targetList);
field.set(target, targetList);
}
targetList.addAll(srcList);
}
}
} catch (Exception e) {
log.warn("[MCI Tool] 리스트 병합 중 예외 발생 (field: {}): {}", field.getName(), e.getMessage());
}
}
}
current = current.getSuperclass();
}
}
}

View File

@@ -1,90 +0,0 @@
package io.shinhanlife.dat.lib.session.dto;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dat.lib.session.dto
* @className SessionDto
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SessionDto {
/* 인사번호 */
private String prafNo;
/* 인사명 */
private String prafNm;
/* 조직번호 */
private String ognzNo;
/* 조직번호 */
private String ognzNm;
/* 이메일주소 */
private String addre;
/* 인사직무코드 */
private String prafOfduCd;
/* 인사직무명 */
private String prafOfduNm;
/* 인사직급코드 */
private String prafOfleCd;
/* 인사직급명 */
private String prafOfleNm;
/* 인사직책코드 */
private String prafDutyCd;
/* 인사직책명 */
private String prafDutyNm;
private List<String> roleNoList;
private List<String> roleNmList;
private List<String> tgtrPrafNoList;
private List<String> tgtrOgnzNoList;
// 추가된 LICO 연동 공통 헤더 필수 필드들
private String strYmd;
private String brafNo;
private String psmrAsrtCd;
private String sbsnRulpAsrtCd;
private String bsduCd;
private String bsquCd;
private String ognzAsrtCd;
private String ognzLeveCd;
private String prgrId;
// 유틸성
private String loginDtm; // 로그인일시
private String isManager; // 관리자여부
public void setLoginDtm() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
this.loginDtm = LocalDateTime.now().format(formatter);
}
public void setIsManager(String isManager) {
// TODO 역할 필터링 후 관리자 여부 체크
this.isManager = "Y";
}
}

View File

@@ -1,59 +0,0 @@
package io.shinhanlife.dat.lib.session.mci;
import io.shinhanlife.dat.lib.session.dto.SessionDto;
import io.shinhanlife.glow.BizException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class AxhubSessionService {
private final MciOnbszClient mciOnbszClient;
public SessionDto fetchUserSession(String employeeNo) {
if (employeeNo == null || employeeNo.trim().isEmpty()) {
return null;
}
try {
ONBSZ0460_I.CmmnPrafIfinInDto input = ONBSZ0460_I.CmmnPrafIfinInDto.builder()
.prafNo(employeeNo)
.build();
ONBSZ0460_I request = ONBSZ0460_I.builder()
.cmmnPrafIfinInDto(List.of(input))
.build();
ONBSZ0460_O response = mciOnbszClient.callOnbsz0460(request);
if (response != null && response.getCmmnPrafIfinOutDto() != null
&& !response.getCmmnPrafIfinOutDto().isEmpty()) {
ONBSZ0460_O.CmmnPrafIfinOutDto info = response.getCmmnPrafIfinOutDto().get(0);
SessionDto sessionDto = new SessionDto();
sessionDto.setOgnzNo(info.getOgnzAsrtCd());
sessionDto.setPrafNo(info.getPrafNo());
sessionDto.setStrYmd(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
sessionDto.setBrafNo(info.getBrafNo());
sessionDto.setPsmrAsrtCd(info.getPsmrAsrtCd());
sessionDto.setSbsnRulpAsrtCd(info.getSbsnRulpAsrtCd());
sessionDto.setBsduCd(info.getBsduCd());
sessionDto.setBsquCd(info.getBsquCd());
sessionDto.setOgnzAsrtCd(info.getOgnzAsrtCd());
sessionDto.setOgnzLeveCd(info.getOgnzLeveCd());
log.info("[AxhubSessionService] MCI ONBSZ0460 조회 성공: 사번={}, 조직코드={}", employeeNo, info.getOgnzAsrtCd());
return sessionDto;
}
} catch (BizException e) {
log.error("[AxhubSessionService] MCI ONBSZ0460 사원 정보 조회 실패 (사번: {})", employeeNo, e);
}
return null;
}
}

View File

@@ -1,22 +0,0 @@
package io.shinhanlife.dat.lib.session.mci;
import io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class MciOnbszClient {
private static final String INTERFACE_ID = "ONBSZ0460";
private static final String RECEIVE_SERVICE_ID = "ONBSZ";
private final AxhubMciComponent mciComponent;
public ONBSZ0460_O callOnbsz0460(ONBSZ0460_I request) {
Transfer<ONBSZ0460_O> transfer = mciComponent.callTo(
INTERFACE_ID, RECEIVE_SERVICE_ID, request, ONBSZ0460_O.class);
return transfer == null ? null : transfer.getBody();
}
}

View File

@@ -1,45 +0,0 @@
package io.shinhanlife.dat.lib.session.mci;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ONBSZ0460_I {
@GlowTrgmField(order = 1, description = "공통인사정보조회InDto", type = "gm")
private List<CmmnPrafIfinInDto> cmmnPrafIfinInDto;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class CmmnPrafIfinInDto {
@GlowTrgmField(order = 1, length = 6, description = "마감년월")
private String closYm;
@GlowTrgmField(order = 2, length = 7, description = "조직번호")
private String ognzNo;
@GlowTrgmField(order = 3, length = 8, description = "인사번호")
private String prafNo;
@GlowTrgmField(order = 4, length = 3, description = "인사조직분류코드")
private String psmrAsrtCd;
@GlowTrgmField(order = 5, length = 1, description = "트레이너구분코드")
private String trnrSccd;
}
}

View File

@@ -1,187 +0,0 @@
package io.shinhanlife.dat.lib.session.mci;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ONBSZ0460_O {
@GlowTrgmField(order = 1, description = "공통인사정보조회OutDto", type = "gm")
private List<CmmnPrafIfinOutDto> cmmnPrafIfinOutDto;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class CmmnPrafIfinOutDto {
@GlowTrgmField(order = 1, length = 8, description = "인사번호")
private String prafNo;
@GlowTrgmField(order = 2, length = 2, description = "인사유형코드")
private String prafTypeCd;
@GlowTrgmField(order = 3, length = 200, description = "인사명")
private String prafNm;
@GlowTrgmField(order = 4, length = 100, description = "인사영문명")
private String prafEngNm;
@GlowTrgmField(order = 5, length = 20, description = "생년월일")
private String birymd;
@GlowTrgmField(order = 6, length = 1, description = "성별코드")
private String gndrCd;
@GlowTrgmField(order = 7, length = 50, description = "주민등록번호")
private String rdreNo;
@GlowTrgmField(order = 8, length = 2, description = "조직분류코드")
private String ognzAsrtCd;
@GlowTrgmField(order = 9, length = 0, description = "TODO 전문 명세 매핑")
private String reserved09;
@GlowTrgmField(order = 10, length = 0, description = "TODO 전문 명세 매핑")
private String reserved10;
@GlowTrgmField(order = 11, length = 0, description = "TODO 전문 명세 매핑")
private String reserved11;
@GlowTrgmField(order = 12, length = 0, description = "TODO 전문 명세 매핑")
private String reserved12;
@GlowTrgmField(order = 13, length = 0, description = "TODO 전문 명세 매핑")
private String reserved13;
@GlowTrgmField(order = 14, length = 0, description = "위촉구분코드 - 길이 확인 필요")
private String appnSccd;
@GlowTrgmField(order = 15, length = 2, description = "위촉경력구분코드")
private String appnCareSccd;
@GlowTrgmField(order = 16, length = 2, description = "위해촉최종상태코드")
private String aprdLsstCd;
@GlowTrgmField(order = 17, length = 20, description = "위촉일자")
private String appnYmd;
@GlowTrgmField(order = 18, length = 20, description = "부임일자")
private String acsoYmd;
@GlowTrgmField(order = 19, length = 20, description = "해임일자")
private String dmssYmd;
@GlowTrgmField(order = 20, length = 20, description = "해촉일자")
private String dsapYmd;
@GlowTrgmField(order = 21, length = 20, description = "재위촉일자")
private String reapYmd;
@GlowTrgmField(order = 22, length = 3, description = "영업직책코드")
private String bsduCd;
@GlowTrgmField(order = 23, length = 10, description = "생명보험협회등록번호")
private String klirNo;
@GlowTrgmField(order = 24, length = 2, description = "신인등급코드")
private String nwfaGrdCd;
@GlowTrgmField(order = 25, length = 2, description = "영업자격코드")
private String bsquCd;
@GlowTrgmField(order = 26, length = 6, description = "자격심사년월")
private String qlfcIspaYm;
@GlowTrgmField(order = 27, length = 2, description = "인사등급코드")
private String prafGrdCd;
@GlowTrgmField(order = 28, length = 0, description = "인사조직분류코드 - 길이 확인 필요")
private String psmrAsrtCd;
@GlowTrgmField(order = 29, length = 0, description = "영업규정분류코드 - 길이 확인 필요")
private String sbsnRulpAsrtCd;
@GlowTrgmField(order = 30, length = 0, description = "조직레벨코드 - 길이 확인 필요")
private String ognzLeveCd;
@GlowTrgmField(order = 31, length = 0, description = "지점번호 - 길이 확인 필요")
private String brafNo;
@GlowTrgmField(order = 32, length = 0, description = "TODO 전문 명세 매핑")
private String reserved32;
@GlowTrgmField(order = 33, length = 0, description = "TODO 전문 명세 매핑")
private String reserved33;
@GlowTrgmField(order = 34, length = 0, description = "TODO 전문 명세 매핑")
private String reserved34;
@GlowTrgmField(order = 35, length = 0, description = "TODO 전문 명세 매핑")
private String reserved35;
@GlowTrgmField(order = 36, length = 0, description = "TODO 전문 명세 매핑")
private String reserved36;
@GlowTrgmField(order = 37, length = 0, description = "TODO 전문 명세 매핑")
private String reserved37;
@GlowTrgmField(order = 38, length = 0, description = "TODO 전문 명세 매핑")
private String reserved38;
@GlowTrgmField(order = 39, length = 0, description = "TODO 전문 명세 매핑")
private String reserved39;
@GlowTrgmField(order = 40, length = 0, description = "TODO 전문 명세 매핑")
private String reserved40;
@GlowTrgmField(order = 41, length = 0, description = "TODO 전문 명세 매핑")
private String reserved41;
@GlowTrgmField(order = 42, length = 0, description = "TODO 전문 명세 매핑")
private String reserved42;
@GlowTrgmField(order = 43, length = 0, description = "TODO 전문 명세 매핑")
private String reserved43;
@GlowTrgmField(order = 44, length = 0, description = "TODO 전문 명세 매핑")
private String reserved44;
@GlowTrgmField(order = 45, length = 0, description = "TODO 전문 명세 매핑")
private String reserved45;
@GlowTrgmField(order = 46, length = 0, description = "TODO 전문 명세 매핑")
private String reserved46;
@GlowTrgmField(order = 47, length = 0, description = "TODO 전문 명세 매핑")
private String reserved47;
@GlowTrgmField(order = 48, length = 0, description = "TODO 전문 명세 매핑")
private String reserved48;
@GlowTrgmField(order = 49, length = 0, description = "TODO 전문 명세 매핑")
private String reserved49;
@GlowTrgmField(order = 50, length = 0, description = "TODO 전문 명세 매핑")
private String reserved50;
@GlowTrgmField(order = 51, length = 0, description = "TODO 전문 명세 매핑")
private String reserved51;
@GlowTrgmField(order = 52, length = 0, description = "TODO 전문 명세 매핑")
private String reserved52;
@GlowTrgmField(order = 53, length = 0, description = "TODO 전문 명세 매핑")
private String reserved53;
@GlowTrgmField(order = 54, length = 0, description = "TODO 전문 명세 매핑")
private String reserved54;
@GlowTrgmField(order = 55, length = 0, description = "TODO 전문 명세 매핑")
private String reserved55;
@GlowTrgmField(order = 56, length = 0, description = "TODO 전문 명세 매핑")
private String reserved56;
@GlowTrgmField(order = 57, length = 0, description = "TODO 전문 명세 매핑")
private String reserved57;
@GlowTrgmField(order = 58, length = 0, description = "TODO 전문 명세 매핑")
private String reserved58;
@GlowTrgmField(order = 59, length = 0, description = "TODO 전문 명세 매핑")
private String reserved59;
@GlowTrgmField(order = 60, length = 0, description = "TODO 전문 명세 매핑")
private String reserved60;
@GlowTrgmField(order = 61, length = 0, description = "TODO 전문 명세 매핑")
private String reserved61;
@GlowTrgmField(order = 62, length = 0, description = "TODO 전문 명세 매핑")
private String reserved62;
@GlowTrgmField(order = 63, length = 0, description = "TODO 전문 명세 매핑")
private String reserved63;
@GlowTrgmField(order = 64, length = 0, description = "TODO 전문 명세 매핑")
private String reserved64;
@GlowTrgmField(order = 65, length = 0, description = "TODO 전문 명세 매핑")
private String reserved65;
@GlowTrgmField(order = 66, length = 0, description = "TODO 전문 명세 매핑")
private String reserved66;
@GlowTrgmField(order = 67, length = 0, description = "TODO 전문 명세 매핑")
private String reserved67;
@GlowTrgmField(order = 68, length = 0, description = "TODO 전문 명세 매핑")
private String reserved68;
@GlowTrgmField(order = 69, length = 0, description = "TODO 전문 명세 매핑")
private String reserved69;
@GlowTrgmField(order = 70, length = 0, description = "TODO 전문 명세 매핑")
private String reserved70;
@GlowTrgmField(order = 71, length = 0, description = "TODO 전문 명세 매핑")
private String reserved71;
@GlowTrgmField(order = 72, length = 0, description = "TODO 전문 명세 매핑")
private String reserved72;
@GlowTrgmField(order = 73, length = 0, description = "TODO 전문 명세 매핑")
private String reserved73;
@GlowTrgmField(order = 74, length = 0, description = "TODO 전문 명세 매핑")
private String reserved74;
@GlowTrgmField(order = 75, length = 0, description = "TODO 전문 명세 매핑")
private String reserved75;
@GlowTrgmField(order = 76, length = 0, description = "TODO 전문 명세 매핑")
private String reserved76;
@GlowTrgmField(order = 77, length = 0, description = "TODO 전문 명세 매핑")
private String reserved77;
@GlowTrgmField(order = 78, length = 0, description = "TODO 전문 명세 매핑")
private String reserved78;
}
}

View File

@@ -1,180 +0,0 @@
package io.shinhanlife.dat.lib.util;
import jakarta.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @package io.shinhanlife.dat.lib.util
* @className CommonCodeResolver
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Slf4j
@Component
public class CommonCodeResolver {
private static final String DEFAULT_RESOURCE_PATH = "codes.csv";
// Group -> (Code -> Name)
private static final Map<String, Map<String, String>> CODE_TO_NAME_MAP = new ConcurrentHashMap<>();
// Group -> (Normalized Name or Code -> Code)
private static final Map<String, Map<String, String>> NAME_TO_CODE_MAP = new ConcurrentHashMap<>();
static {
loadCodes(DEFAULT_RESOURCE_PATH);
}
@PostConstruct
public void init() {
loadCodes(DEFAULT_RESOURCE_PATH);
}
public static synchronized void reload() {
loadCodes(DEFAULT_RESOURCE_PATH);
}
public static synchronized void loadCodes(String resourcePath) {
ClassLoader classLoader = CommonCodeResolver.class.getClassLoader();
try (InputStream is = classLoader.getResourceAsStream(resourcePath)) {
if (is == null) {
log.warn("[CommonCodeResolver] 리소스 파일을 찾을 수 없습니다: {}", resourcePath);
return;
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
CODE_TO_NAME_MAP.clear();
NAME_TO_CODE_MAP.clear();
String line;
boolean firstLine = true;
while ((line = reader.readLine()) != null) {
if (firstLine) {
firstLine = false;
if (line.startsWith("\uFEFF")) {
line = line.substring(1);
}
if (line.contains("그룹") || line.contains("group")) {
continue;
}
}
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] parts = line.split(",", -1);
if (parts.length < 3) {
continue;
}
String group = parts[0].trim().toUpperCase(Locale.ROOT);
String code = parts[1].trim();
String name = parts[2].trim();
if (group.isEmpty() || code.isEmpty()) {
continue;
}
Map<String, String> codeToName = CODE_TO_NAME_MAP.computeIfAbsent(group, k -> new ConcurrentHashMap<>());
Map<String, String> nameToCode = NAME_TO_CODE_MAP.computeIfAbsent(group, k -> new ConcurrentHashMap<>());
codeToName.put(code, name);
nameToCode.put(code, code);
nameToCode.put(name, code);
nameToCode.put(name.toLowerCase(Locale.ROOT), code);
nameToCode.put(name.replace(" ", ""), code);
}
log.info("[CommonCodeResolver] 공통코드 로드 완료: {}개 그룹", CODE_TO_NAME_MAP.size());
}
} catch (Exception e) {
log.error("[CommonCodeResolver] 공통코드 로드 중 오류 발생: {}", e.getMessage(), e);
}
}
/**
* 입력값(명칭 또는 코드)을 처리계 표준 코드값으로 변환합니다.
*
* @param group 코드 그룹명 (예: COMPANY, STATUS)
* @param input 사용자가 입력한 명칭("삼성전자") 또는 코드("005930")
* @return 매핑된 코드값(예: "005930"). 매핑되지 않으면 원본 입력값 반환.
*/
public static String resolve(String group, String input) {
return resolve(group, input, input);
}
/**
* 입력값(명칭 또는 코드)을 처리계 표준 코드값으로 변환합니다.
*
* @param group 코드 그룹명
* @param input 입력값
* @param defaultValue 매핑 실패 시 반환할 기본값
* @return 매핑된 코드값 또는 defaultValue
*/
public static String resolve(String group, String input, String defaultValue) {
if (input == null || input.isBlank()) {
return defaultValue;
}
if (group == null) {
return input.trim();
}
String normalizedGroup = group.trim().toUpperCase(Locale.ROOT);
Map<String, String> nameToCode = NAME_TO_CODE_MAP.get(normalizedGroup);
if (nameToCode == null) {
return defaultValue;
}
String trimmed = input.trim();
String matched = nameToCode.get(trimmed);
if (matched != null) {
return matched;
}
matched = nameToCode.get(trimmed.toLowerCase(Locale.ROOT));
if (matched != null) {
return matched;
}
matched = nameToCode.get(trimmed.replace(" ", ""));
if (matched != null) {
return matched;
}
return defaultValue;
}
/**
* 코드값에 해당하는 표준 명칭을 반환합니다. (응답 변환 시 사용)
*/
public static String getName(String group, String code) {
if (group == null || code == null) {
return code;
}
String normalizedGroup = group.trim().toUpperCase(Locale.ROOT);
Map<String, String> codeToName = CODE_TO_NAME_MAP.get(normalizedGroup);
if (codeToName != null) {
return codeToName.getOrDefault(code.trim(), code);
}
return code;
}
/**
* 특정 그룹의 전체 코드-명칭 맵을 반환합니다.
*/
public static Map<String, String> getCodeMap(String group) {
if (group == null) {
return Collections.emptyMap();
}
Map<String, String> map = CODE_TO_NAME_MAP.get(group.trim().toUpperCase(Locale.ROOT));
return map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap();
}
}

View File

@@ -1,211 +0,0 @@
package io.shinhanlife.dat.lib.util;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import org.springaicommunity.mcp.annotation.McpToolParam;
import io.swagger.v3.oas.annotations.media.Schema;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jakarta.validation.constraints.Pattern;
/**
* @package io.shinhanlife.dat.lib.util
* @className JsonSchemaGenerator
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class JsonSchemaGenerator {
/**
* Java DTO 클래스를 분석하여 MCP 규격의 완전한 JSON Schema를 생성합니다.
*/
public static Map<String, Object> generateSchema(Class<?> clazz) {
return generateSchema(clazz, new HashSet<>());
}
private static Map<String, Object> generateSchema(Class<?> clazz, Set<Class<?>> visiting) {
Map<String, Object> schema = new HashMap<>();
schema.put("type", "object");
schema.put("additionalProperties", false);
if (!visiting.add(clazz)) {
return schema;
}
Map<String, Object> properties = new HashMap<>();
List<String> requiredList = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
Map<String, Object> fieldSchema = createFieldSchema(field, visiting);
// 1. 타입 매핑
// 2. 어노테이션 기반 설명 추출
McpToolParam paramAnnotation = field.getAnnotation(McpToolParam.class);
JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class);
if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
fieldSchema.put("description", paramAnnotation.description());
} else if (descAnnotation != null && !descAnnotation.value().isEmpty()) {
fieldSchema.put("description", descAnnotation.value());
} else {
fieldSchema.put("description", field.getName()); // 기본값
}
// 3. 필수 여부 판단
JsonProperty jsonProp = field.getAnnotation(JsonProperty.class);
if ((jsonProp != null && jsonProp.required()) || (paramAnnotation != null && paramAnnotation.required())) {
requiredList.add(field.getName());
}
Pattern patternAnnotation = field.getAnnotation(Pattern.class);
if (patternAnnotation != null && !patternAnnotation.regexp().isEmpty()) {
fieldSchema.put("pattern", patternAnnotation.regexp());
}
Schema schemaAnnotation = field.getAnnotation(Schema.class);
if (schemaAnnotation != null) {
if (!schemaAnnotation.description().isEmpty() && !fieldSchema.containsKey("description")) {
fieldSchema.put("description", schemaAnnotation.description());
}
if ((schemaAnnotation.required() || schemaAnnotation.requiredMode() == Schema.RequiredMode.REQUIRED)
&& !requiredList.contains(field.getName())) {
requiredList.add(field.getName());
}
if (!schemaAnnotation.pattern().isEmpty()) {
fieldSchema.put("pattern", schemaAnnotation.pattern());
}
if (!schemaAnnotation.minimum().isEmpty()) {
try {
fieldSchema.put("minimum", Long.valueOf(schemaAnnotation.minimum()));
} catch (NumberFormatException ignored) {}
}
if (!schemaAnnotation.maximum().isEmpty()) {
try {
fieldSchema.put("maximum", Long.valueOf(schemaAnnotation.maximum()));
} catch (NumberFormatException ignored) {}
}
if (schemaAnnotation.minLength() > 0) {
fieldSchema.put("minLength", schemaAnnotation.minLength());
}
if (schemaAnnotation.maxLength() > 0 && schemaAnnotation.maxLength() != Integer.MAX_VALUE) {
fieldSchema.put("maxLength", schemaAnnotation.maxLength());
}
if (schemaAnnotation.allowableValues().length > 0 && !schemaAnnotation.allowableValues()[0].isEmpty()) {
fieldSchema.put("enum", List.of(schemaAnnotation.allowableValues()));
}
if (!schemaAnnotation.format().isEmpty()) {
fieldSchema.put("format", schemaAnnotation.format());
}
if (!schemaAnnotation.defaultValue().isEmpty()) {
fieldSchema.put("default", coerceDefaultValue(schemaAnnotation.defaultValue(), field.getType()));
}
if (!schemaAnnotation.example().isEmpty()) {
fieldSchema.put("examples", List.of(schemaAnnotation.example()));
}
if (schemaAnnotation.nullable()) {
Map<String, Object> nonNullSchema = new HashMap<>(fieldSchema);
fieldSchema = new HashMap<>();
fieldSchema.put("anyOf", List.of(
nonNullSchema,
Map.of("type", "null")
));
}
}
properties.put(field.getName(), fieldSchema);
}
schema.put("properties", properties);
if (!requiredList.isEmpty()) {
schema.put("required", requiredList);
}
// anyOf removed
visiting.remove(clazz);
return schema;
}
private static Object coerceDefaultValue(String value, Class<?> fieldType) {
try {
if (fieldType == Integer.class || fieldType == int.class
|| fieldType == Long.class || fieldType == long.class
|| fieldType == Short.class || fieldType == short.class
|| fieldType == Byte.class || fieldType == byte.class) {
return Long.valueOf(value);
}
if (fieldType == Double.class || fieldType == double.class
|| fieldType == Float.class || fieldType == float.class) {
return Double.valueOf(value);
}
if (fieldType == Boolean.class || fieldType == boolean.class) {
return Boolean.valueOf(value);
}
return value;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid MCP default value: " + value, e);
}
}
private static Map<String, Object> createFieldSchema(Field field, Set<Class<?>> visiting) {
Class<?> fieldType = field.getType();
if (isSimpleType(fieldType)) {
return new HashMap<>(Map.of("type", mapJavaTypeToJsonType(fieldType)));
}
if (List.class.isAssignableFrom(fieldType)) {
Map<String, Object> fieldSchema = new HashMap<>();
fieldSchema.put("type", "array");
fieldSchema.put("items", generateItemsSchema(field, visiting));
return fieldSchema;
}
return generateSchema(fieldType, visiting);
}
private static Map<String, Object> generateItemsSchema(Field field, Set<Class<?>> visiting) {
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType parameterizedType) {
Type itemType = parameterizedType.getActualTypeArguments()[0];
if (itemType instanceof Class<?> itemClass) {
if (isSimpleType(itemClass)) {
return new HashMap<>(Map.of("type", mapJavaTypeToJsonType(itemClass)));
}
return generateSchema(itemClass, visiting);
}
}
return new HashMap<>(Map.of("type", "object"));
}
private static boolean isSimpleType(Class<?> clazz) {
return clazz == String.class
|| clazz == Integer.class || clazz == int.class
|| clazz == Long.class || clazz == long.class
|| clazz == Double.class || clazz == double.class
|| clazz == Float.class || clazz == float.class
|| clazz == Boolean.class || clazz == boolean.class;
}
private static String mapJavaTypeToJsonType(Class<?> clazz) {
if (clazz == String.class) return "string";
if (clazz == Integer.class || clazz == int.class) return "integer";
if (clazz == Long.class || clazz == long.class) return "integer";
if (clazz == Double.class || clazz == double.class) return "number";
if (clazz == Float.class || clazz == float.class) return "number";
if (clazz == Boolean.class || clazz == boolean.class) return "boolean";
if (List.class.isAssignableFrom(clazz)) return "array";
return "object";
}
}

View File

@@ -1,488 +0,0 @@
package io.shinhanlife.dat.lib.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PodScaffolder {
private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory());
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
String rawModuleName = getOrAsk(args, 0, scanner, "1. 생성할 모듈(Pod) 이름 (예: payment 또는 dat-was-payment): ");
String moduleName = rawModuleName.startsWith("dat-was-") ? rawModuleName : "dat-was-" + rawModuleName;
String portStr = getOrAsk(args, 1, scanner, "2. 사용할 포트 번호 (예: 8085): ");
String shortName = moduleName.replace("dat-was-", "").replace("-", "");
String defaultAuthor = System.getProperty("user.name");
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
String author = getOrAsk(args, 2, scanner, "3. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
if (author.trim().isEmpty()) author = defaultAuthor;
String createDate = getOrAsk(args, 3, scanner, "4. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
if (createDate.trim().isEmpty()) createDate = defaultDate;
String result = scaffoldPod(moduleName, portStr, shortName, author, createDate);
}
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
if (args.length > index) {
return args[index];
}
return scanner.nextLine().trim();
}
public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, String createDate) throws IOException {
String envSourceDir = System.getProperty("AXHUB_SOURCE_DIR");
if (envSourceDir == null || envSourceDir.isBlank()) {
envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
}
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate, null);
}
public static String scaffoldPod(String moduleName, String portStr, String shortName, String author,
String createDate, String toolServiceManifest) throws IOException {
String envSourceDir = System.getProperty("AXHUB_SOURCE_DIR");
if (envSourceDir == null || envSourceDir.isBlank()) envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate, toolServiceManifest);
}
static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName,
String author, String createDate) throws IOException {
return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate, null);
}
static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName,
String author, String createDate, String toolServiceManifest) throws IOException {
Path modulePath = rootDir.resolve(Paths.get(moduleName));
if (Files.exists(modulePath)) {
return "[오류] 이미 존재하는 모듈입니다: " + moduleName;
}
StringBuilder log = new StringBuilder();
log.append("[1/6] 모듈 디렉터리 생성 중...\n");
Files.createDirectories(modulePath);
log.append("[2/6] build.gradle 생성 중...\n");
String buildGradle = """
plugins {
// Spring Boot 3.5.11 version is managed by the root build.gradle.
id 'org.springframework.boot'
}
dependencies {
// Shared MCP, Glow integration, validation, logging, Lombok and MapStruct configuration.
implementation project(':dat-was-lib')
}
""";
writeUtf8(modulePath.resolve("build.gradle"), buildGradle);
log.append("[3/6] Dockerfile 생성 중...\n");
String dockerfile = """
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
COPY %s/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE %s
ENTRYPOINT ["java", "-jar", "app.jar"]
""".formatted(moduleName, portStr);
writeUtf8(modulePath.resolve("Dockerfile"), dockerfile);
log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n");
Path srcPath = modulePath.resolve("src/main/java/io/shinhanlife/dat/mcc/" + shortName);
Files.createDirectories(srcPath);
String appClass = """
package io.shinhanlife.dat.mcc.%s;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
/**
* @package io.shinhanlife.dat.mcc.%s
* @className DatWas%sApplication
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dat.mcc", "io.shinhanlife.dat.lib"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dat.mcc", "io.shinhanlife.dat.lib"})
@EnableCaching
public class DatWas%sApplication {
public static void main(String[] args) {
SpringApplication.run(DatWas%sApplication.class, args);
}
}
""".formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName));
writeUtf8(srcPath.resolve("DatWas" + capitalize(shortName) + "Application.java"), appClass);
Path resPath = modulePath.resolve("src/main/resources");
Files.createDirectories(resPath);
String applicationYml = """
server:
port: %s
spring:
application:
name: %s
config:
import: optional:classpath:tool-service-manifest.yml
profiles:
active: local
logging:
level:
org.apache.kafka: ERROR
mcp:
namespace: ""
manifest:
bundle-id: %s
name-prefix: ""
security:
api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
tenant-domains:
TESTER-DEV: ALL
axhub:
tool:
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
""".formatted(portStr, moduleName, moduleName.replace("dap-", ""));
writeUtf8(resPath.resolve("application.yml"), applicationYml);
String manifest = toolServiceManifest == null || toolServiceManifest.isBlank()
? defaultToolServiceManifest(moduleName) : toolServiceManifest.trim() + System.lineSeparator();
writeUtf8(resPath.resolve("tool-service-manifest.yml"), manifest);
String applicationLocalYml = """
# Local 환경 전용 설정 (H2 메모리 DB 등)
spring:
config:
activate:
on-profile: local
import:
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-local.yml
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;
username: sa
password: password
h2:
console:
enabled: true
eims:
http:
url: http://localhost:${server.port}/api/gateway
tcp:
host: 127.0.0.1
port: 8090
timeout: 5000
jsp:
form:
url: http://localhost:${server.port}/mock/jsp-form
json:
url: http://localhost:${server.port}/mock/jsp-json
mci:
url: http://localhost:${server.port}/api/mock/esb/api
mcistring:
url: http://localhost:${server.port}/api/mock/esb/string
mcp:
security:
tenant-domains:
mcp-client-1: CUSTOMER,COMMON
mcp-client-2: ALL
axhub:
gateway:
url: http://localhost:8081
tool:
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
""";
writeUtf8(resPath.resolve("application-local.yml"), applicationLocalYml);
String applicationDevYml = """
# OCI 클라우드 환경 전용 설정
server:
port: ${PORT:%s}
spring:
config:
activate:
on-profile: dev
import:
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-dev.yml
axhub:
gateway:
url: https://axhubmcp.devjun.net
tool:
url: http://144.24.70.100:%s
eims:
http:
url: http://localhost:${server.port}/api/gateway
tcp:
host: 127.0.0.1
port: 8090
timeout: 5000
jsp:
form:
url: http://localhost:${server.port}/mock/jsp-form
json:
url: http://localhost:${server.port}/mock/jsp-json
mci:
url: http://localhost:${server.port}/api/mock/esb/api
mcistring:
url: http://localhost:${server.port}/api/mock/esb/string
shinhan:
integration:
envrTypeCd: D
eai:
url: http://10.176.32.181
internalMci:
url: http://10.176.32.173
bancaMci:
url: http://10.176.32.117
externalMci:
url: http://10.176.32.176
""".formatted(portStr, portStr);
writeUtf8(resPath.resolve("application-dev.yml"), applicationDevYml);
String applicationTestYml = """
server:
port: ${PORT:%s}
spring:
config:
activate:
on-profile: test
import:
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-test.yml
axhub:
gateway:
url: ${AXHUB_GATEWAY_URL}
tool:
url: ${AXHUB_TOOL_URL}
""".formatted(portStr);
writeUtf8(resPath.resolve("application-test.yml"), applicationTestYml);
String applicationProdYml = """
server:
port: ${PORT:%s}
spring:
config:
activate:
on-profile: prod
import:
- classpath:glow/application-glow.yml
- classpath:glow/application-glow-prod.yml
axhub:
gateway:
url: ${AXHUB_GATEWAY_URL}
tool:
url: ${AXHUB_TOOL_URL}
""".formatted(portStr);
writeUtf8(resPath.resolve("application-prod.yml"), applicationProdYml);
String logbackXml = """
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATTERN" value="%%d{yyyy-MM-dd HH:mm:ss.SSS} [%%thread] [%%X{traceId}] %%-5level %%logger{36} - %%msg%%n" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/%s.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/%s-%%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<logger name="io.shinhanlife" level="DEBUG" />
</configuration>
""".formatted(moduleName, moduleName);
writeUtf8(resPath.resolve("logback-spring.xml"), logbackXml);
log.append("[5/6] settings.gradle 에 모듈 등록 중...\n");
Path settingsPath = rootDir.resolve(Paths.get("settings.gradle"));
if (Files.exists(settingsPath)) {
String settings = Files.readString(settingsPath, StandardCharsets.UTF_8);
if (!settings.contains("include '" + moduleName + "'")) {
Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardCharsets.UTF_8, StandardOpenOption.APPEND);
}
}
log.append("[6/6] docker-compose.yml 에 서비스 추가 중...\n");
Path dockerComposePath = rootDir.resolve(Paths.get("docker-compose.yml"));
if (Files.exists(dockerComposePath)) {
String compose = Files.readString(dockerComposePath, StandardCharsets.UTF_8);
String serviceName = moduleName.replace("dap-", ""); // e.g. tool-payment
if (!compose.contains(" " + serviceName + ":")) {
String newService = """
%s:
build:
context: .
dockerfile: %s/Dockerfile
ports:
- "%s:%s"
environment:
- TZ=Asia/Seoul
- TOOL_SERVER_API_KEY=${TOOL_SERVER_API_KEY:-tool-server-key}
- AXHUB_GATEWAY_URL=http://gateway:8081
- AXHUB_TOOL_URL=http://%s:%s
- GLOW_COMMUNICATION_MCI_HOST=http://mci-mock
- GLOW_COMMUNICATION_MCI_PORT=8080
- GLOW_COMMUNICATION_EXTMCI_HOST=http://mci-mock
- GLOW_COMMUNICATION_EXTMCI_PORT=8080
- GLOW_COMMUNICATION_EAI_HOST=http://mci-mock
- GLOW_COMMUNICATION_EAI_PORT=8080
""".formatted(serviceName, moduleName, portStr, portStr, serviceName, portStr);
Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardCharsets.UTF_8, StandardOpenOption.APPEND);
}
}
List<String> updatedManifests = updateReciprocalConfusableServers(rootDir, moduleName, manifest);
if (!updatedManifests.isEmpty()) {
log.append("[추가] 기존 Pod confusable-servers 갱신: ")
.append(String.join(", ", updatedManifests)).append("\n");
}
log.append("\n=========================================\n");
log.append(" Pod Scaffolding Complete! \n");
log.append("=========================================\n");
log.append("1. [새로운 모듈] ").append(moduleName).append(" 폴더가 생성되었습니다.\n");
log.append("2. [ToolScaffolder]를 사용해 이 모듈 안에 툴을 추가하세요.\n");
log.append("3. 실행 전 Gradle 동기화(Sync)를 한 번 진행해 주세요.\n");
return log.toString();
}
private static void writeUtf8(Path path, String content) throws IOException {
Files.writeString(path, content, StandardCharsets.UTF_8);
}
private static String defaultToolServiceManifest(String moduleName) {
String key = moduleName.replace("dat-was-", "");
return """
mcp:
manifest:
routing-functions:
- name: route_to_%s
description-serialization: %s 업무 서버로 요청을 라우팅합니다. %s 관련 업무를 처리합니다. 요청의 주요 업무 영역을 기준으로 서버를 선택합니다.
server-id: %s
category-key: %s
product-boundary: insurance
business-domain: %s 업무
business-outcome: %s 관련 업무를 처리합니다.
primary-entities: []
capabilities: []
select-if: %s 관련 요청인 경우
reject-if: 다른 업무 영역이 주된 요청인 경우
confusable-servers: []
decision-policy: 요청의 주요 업무 영역을 기준으로 서버를 선택합니다.
""".formatted(moduleName, key, key, moduleName, key, key, key, key);
}
private static List<String> updateReciprocalConfusableServers(Path rootDir, String moduleName,
String newManifest) throws IOException {
List<String> updated = new ArrayList<>();
for (String target : extractConfusableServers(newManifest)) {
if (target.equals(moduleName) || !target.matches("^dat-was-[a-z0-9-]+$")) continue;
Path path = rootDir.resolve(target).resolve("src/main/resources/tool-service-manifest.yml");
if (!Files.isRegularFile(path)) continue;
String before = Files.readString(path, StandardCharsets.UTF_8);
String after = addConfusableServer(before, moduleName);
if (!before.equals(after)) {
writeUtf8(path, after);
updated.add(rootDir.relativize(path).toString().replace('\\', '/'));
}
}
return updated;
}
@SuppressWarnings("unchecked")
private static List<String> extractConfusableServers(String manifest) throws IOException {
Map<String, Object> root = YAML_MAPPER.readValue(manifest, Map.class);
Object mcpValue = root.get("mcp");
if (!(mcpValue instanceof Map<?, ?> mcp)) return List.of();
Object manifestValue = mcp.get("manifest");
if (!(manifestValue instanceof Map<?, ?> manifestMap)) return List.of();
Object functionsValue = manifestMap.get("routing-functions");
if (!(functionsValue instanceof List<?> functions) || functions.isEmpty()
|| !(functions.getFirst() instanceof Map<?, ?> function)) return List.of();
Object serversValue = function.get("confusable-servers");
if (!(serversValue instanceof Collection<?> servers)) return List.of();
LinkedHashSet<String> values = new LinkedHashSet<>();
for (Object value : servers) {
String normalized = value == null ? "" : value.toString().trim();
if (!normalized.isBlank()) values.add(normalized);
}
return List.copyOf(values);
}
private static String addConfusableServer(String manifest, String moduleName) {
Pattern pattern = Pattern.compile("(?m)^(\\s*)confusable-servers:\\s*\\[([^]]*)]\\s*$");
Matcher matcher = pattern.matcher(manifest);
if (matcher.find()) {
LinkedHashSet<String> values = new LinkedHashSet<>();
for (String value : matcher.group(2).split(",")) {
String normalized = value.trim();
if (!normalized.isBlank()) values.add(normalized);
}
if (!values.add(moduleName)) return manifest;
String replacement = matcher.group(1) + "confusable-servers: [" + String.join(", ", values) + "]";
return matcher.replaceFirst(Matcher.quoteReplacement(replacement));
}
Matcher category = Pattern.compile("(?m)^(\\s*)category-key:[^\\r\\n]*$").matcher(manifest);
if (!category.find()) return manifest;
String replacement = category.group() + System.lineSeparator() + category.group(1)
+ "confusable-servers: [" + moduleName + "]";
return category.replaceFirst(Matcher.quoteReplacement(replacement));
}
private static String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}

View File

@@ -1,55 +0,0 @@
package io.shinhanlife.dat.lib.util;
import io.shinhanlife.dat.lib.session.dto.SessionDto;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* @package io.shinhanlife.dat.lib.util
* @className SessionUtil
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class SessionUtil {
private static final String SESSION_KEY = "userInfo";
private SessionUtil() {}
public static SessionDto getSession() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) return null;
// 1. Check HTTP Request Attribute first (populated by McpRequestHeaderFilter)
HttpServletRequest request = attributes.getRequest();
SessionDto requestSession = (SessionDto) request.getAttribute(SESSION_KEY);
if (requestSession != null) {
return requestSession;
}
// 2. Fallback to standard HttpSession
HttpSession session = request.getSession(false);
if (session == null) return null;
return (SessionDto) session.getAttribute(SESSION_KEY);
}
public static String getPrafNo() {
SessionDto session = getSession();
return session != null ? session.getPrafNo() : null;
}
public static String getOgnzNo() {
SessionDto session = getSession();
return session != null ? session.getOgnzNo() : null;
}
}

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