forked from kimhyungsik/ax_hub_mcp_tool
Compare commits
3 Commits
5f58c22efe
...
5955c2168e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5955c2168e | ||
|
|
244bfade74 | ||
|
|
a8555f05af |
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,69 +0,0 @@
|
||||
package io.shinhanlife.dat.lib.paging;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dat.lib.paging
|
||||
* @className MciPagingUtils
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
public final class MciPagingUtils {
|
||||
|
||||
private MciPagingUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이징 응답 데이터(List 필드)를 대상(target) 객체에 순차적으로 병합(append)합니다.
|
||||
* 클래스 상속 계층 구조를 탐색하여 모든 List 타입 필드를 병합합니다.
|
||||
*
|
||||
* @param target 누적할 최종 응답 DTO
|
||||
* @param src 각 페이지별 응답 DTO
|
||||
*/
|
||||
public static void mergeResponseData(Object target, Object src) {
|
||||
if (target == null || src == null) {
|
||||
return;
|
||||
}
|
||||
Class<?> current = target.getClass();
|
||||
while (current != null && current != Object.class) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (List.class.isAssignableFrom(field.getType())) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
List<?> srcList = (List<?>) field.get(src);
|
||||
if (srcList != null && !srcList.isEmpty()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> targetList = (List<Object>) field.get(target);
|
||||
if (targetList == null) {
|
||||
targetList = new ArrayList<>(srcList);
|
||||
field.set(target, targetList);
|
||||
} else {
|
||||
if (!(targetList instanceof ArrayList)) {
|
||||
targetList = new ArrayList<>(targetList);
|
||||
field.set(target, targetList);
|
||||
}
|
||||
targetList.addAll(srcList);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[MCI Tool] 리스트 병합 중 예외 발생 (field: {}): {}", field.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,180 +0,0 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dat.lib.util
|
||||
* @className CommonCodeResolver
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CommonCodeResolver {
|
||||
|
||||
private static final String DEFAULT_RESOURCE_PATH = "codes.csv";
|
||||
|
||||
// Group -> (Code -> Name)
|
||||
private static final Map<String, Map<String, String>> CODE_TO_NAME_MAP = new ConcurrentHashMap<>();
|
||||
// Group -> (Normalized Name or Code -> Code)
|
||||
private static final Map<String, Map<String, String>> NAME_TO_CODE_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
loadCodes(DEFAULT_RESOURCE_PATH);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
loadCodes(DEFAULT_RESOURCE_PATH);
|
||||
}
|
||||
|
||||
public static synchronized void reload() {
|
||||
loadCodes(DEFAULT_RESOURCE_PATH);
|
||||
}
|
||||
|
||||
public static synchronized void loadCodes(String resourcePath) {
|
||||
ClassLoader classLoader = CommonCodeResolver.class.getClassLoader();
|
||||
try (InputStream is = classLoader.getResourceAsStream(resourcePath)) {
|
||||
if (is == null) {
|
||||
log.warn("[CommonCodeResolver] 리소스 파일을 찾을 수 없습니다: {}", resourcePath);
|
||||
return;
|
||||
}
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||
CODE_TO_NAME_MAP.clear();
|
||||
NAME_TO_CODE_MAP.clear();
|
||||
|
||||
String line;
|
||||
boolean firstLine = true;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (firstLine) {
|
||||
firstLine = false;
|
||||
if (line.startsWith("\uFEFF")) {
|
||||
line = line.substring(1);
|
||||
}
|
||||
if (line.contains("그룹") || line.contains("group")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = line.split(",", -1);
|
||||
if (parts.length < 3) {
|
||||
continue;
|
||||
}
|
||||
String group = parts[0].trim().toUpperCase(Locale.ROOT);
|
||||
String code = parts[1].trim();
|
||||
String name = parts[2].trim();
|
||||
|
||||
if (group.isEmpty() || code.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, String> codeToName = CODE_TO_NAME_MAP.computeIfAbsent(group, k -> new ConcurrentHashMap<>());
|
||||
Map<String, String> nameToCode = NAME_TO_CODE_MAP.computeIfAbsent(group, k -> new ConcurrentHashMap<>());
|
||||
|
||||
codeToName.put(code, name);
|
||||
nameToCode.put(code, code);
|
||||
nameToCode.put(name, code);
|
||||
nameToCode.put(name.toLowerCase(Locale.ROOT), code);
|
||||
nameToCode.put(name.replace(" ", ""), code);
|
||||
}
|
||||
log.info("[CommonCodeResolver] 공통코드 로드 완료: {}개 그룹", CODE_TO_NAME_MAP.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[CommonCodeResolver] 공통코드 로드 중 오류 발생: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력값(명칭 또는 코드)을 처리계 표준 코드값으로 변환합니다.
|
||||
*
|
||||
* @param group 코드 그룹명 (예: COMPANY, STATUS)
|
||||
* @param input 사용자가 입력한 명칭("삼성전자") 또는 코드("005930")
|
||||
* @return 매핑된 코드값(예: "005930"). 매핑되지 않으면 원본 입력값 반환.
|
||||
*/
|
||||
public static String resolve(String group, String input) {
|
||||
return resolve(group, input, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력값(명칭 또는 코드)을 처리계 표준 코드값으로 변환합니다.
|
||||
*
|
||||
* @param group 코드 그룹명
|
||||
* @param input 입력값
|
||||
* @param defaultValue 매핑 실패 시 반환할 기본값
|
||||
* @return 매핑된 코드값 또는 defaultValue
|
||||
*/
|
||||
public static String resolve(String group, String input, String defaultValue) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (group == null) {
|
||||
return input.trim();
|
||||
}
|
||||
String normalizedGroup = group.trim().toUpperCase(Locale.ROOT);
|
||||
Map<String, String> nameToCode = NAME_TO_CODE_MAP.get(normalizedGroup);
|
||||
if (nameToCode == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
String trimmed = input.trim();
|
||||
String matched = nameToCode.get(trimmed);
|
||||
if (matched != null) {
|
||||
return matched;
|
||||
}
|
||||
matched = nameToCode.get(trimmed.toLowerCase(Locale.ROOT));
|
||||
if (matched != null) {
|
||||
return matched;
|
||||
}
|
||||
matched = nameToCode.get(trimmed.replace(" ", ""));
|
||||
if (matched != null) {
|
||||
return matched;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드값에 해당하는 표준 명칭을 반환합니다. (응답 변환 시 사용)
|
||||
*/
|
||||
public static String getName(String group, String code) {
|
||||
if (group == null || code == null) {
|
||||
return code;
|
||||
}
|
||||
String normalizedGroup = group.trim().toUpperCase(Locale.ROOT);
|
||||
Map<String, String> codeToName = CODE_TO_NAME_MAP.get(normalizedGroup);
|
||||
if (codeToName != null) {
|
||||
return codeToName.getOrDefault(code.trim(), code);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 그룹의 전체 코드-명칭 맵을 반환합니다.
|
||||
*/
|
||||
public static Map<String, String> getCodeMap(String group) {
|
||||
if (group == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, String> map = CODE_TO_NAME_MAP.get(group.trim().toUpperCase(Locale.ROOT));
|
||||
return map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user