forked from kimhyungsik/ax_hub_mcp_tool
소스 업데이트
This commit is contained in:
520
README.md
520
README.md
@@ -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 결과·요청 헤더 전달·오류 계약을 확인합니다.
|
||||
|
||||
86
build.gradle
86
build.gradle
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"resultCode" : "SUCCESS",
|
||||
"data" : "[{\"date\":\"2024-01-05\",\"message\":\"안내 내용\"}]"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"resultCode" : "SUCCESS",
|
||||
"claimId" : "CLM20230001"
|
||||
}
|
||||
@@ -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: 요청의 주요 엔티티와 업무 영역을 기준으로 고객 서버를 선택합니다.
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 요청 캐시 시간 (초 단위)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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; // 예비항목명
|
||||
}
|
||||
@@ -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; // 부가메시지내용
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; // 예비 필드
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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"); // 아까 설정한 캐시 이름 등록
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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를 입력해주세요. ")));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import io.shinhanlife.dat.lib.metadata.ToolDefinition;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CI/CD 및 로컬 개발 환경에서 V17 Tool YAML 정의 파일을 읽어 Java @McpTool 어노테이션을 동기화합니다.
|
||||
*/
|
||||
public final class ToolAnnotationSyncRunner {
|
||||
|
||||
private ToolAnnotationSyncRunner() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 1) {
|
||||
throw new IllegalArgumentException("Usage: ToolAnnotationSyncRunner <project-root>");
|
||||
}
|
||||
sync(Path.of(args[0]));
|
||||
}
|
||||
|
||||
static void sync(Path projectRoot) {
|
||||
ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||
int syncCount = 0;
|
||||
try (Stream<Path> files = Files.walk(projectRoot)) {
|
||||
List<Path> yamlFiles = files.filter(Files::isRegularFile)
|
||||
.filter(path -> path.toString().replace('\\', '/').contains("/src/main/resources/tool-definitions/"))
|
||||
.filter(path -> path.toString().endsWith(".yml") || path.toString().endsWith(".yaml"))
|
||||
.toList();
|
||||
|
||||
for (Path file : yamlFiles) {
|
||||
ToolDefinition definition = yamlMapper.readValue(file.toFile(), ToolDefinition.class);
|
||||
|
||||
String toolName = definition.name();
|
||||
String title = definition.displayName();
|
||||
String description = definition.displayDescription();
|
||||
boolean readOnlyHint = Boolean.TRUE.equals(definition.readOnly());
|
||||
boolean destructiveHint = Boolean.TRUE.equals(definition.destructive());
|
||||
boolean idempotentHint = Boolean.TRUE.equals(definition.idempotent());
|
||||
String categoryKey = definition.categoryKey();
|
||||
|
||||
try {
|
||||
boolean updated = ToolSourceUpdater.syncToolSource(projectRoot, toolName, title, description,
|
||||
readOnlyHint, destructiveHint, idempotentHint, categoryKey);
|
||||
if (updated) {
|
||||
syncCount++;
|
||||
System.out.println("Synced (Updated): " + toolName);
|
||||
} else {
|
||||
System.out.println("Synced (Unchanged): " + toolName);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.err.println("Skipped (Not Found): " + toolName);
|
||||
}
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Failed to run tool annotation sync", exception);
|
||||
}
|
||||
System.out.println("Tool annotation sync completed: " + syncCount + " tools updated.");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user