forked from kimhyungsik/ax_hub_mcp_tool
refactor: rename dasmt to datmt across the project
All checks were successful
Deploy Tools / deploy (push) Successful in 1m11s
All checks were successful
Deploy Tools / deploy (push) Successful in 1m11s
This commit is contained in:
435
README.md
435
README.md
@@ -1,10 +1,10 @@
|
||||
# DASMT MCP Tool Pods
|
||||
# DATMT MCP Tool Pods
|
||||
|
||||
신한라이프 업무 기능을 MCP(Model Context Protocol) Tool로 제공하는 Java 멀티 모듈 프로젝트입니다. 각 업무 모듈은 독립 실행 가능한 Spring Boot 애플리케이션이며, MCP Streamable HTTP와 REST 실행 API를 함께 제공합니다.
|
||||
?좏븳?쇱씠???낅Т 湲곕뒫??MCP(Model Context Protocol) Tool濡??쒓났?섎뒗 Java 硫??紐⑤뱢 ?꾨줈?앺듃?낅땲?? 媛??낅Т 紐⑤뱢? ?낅┰ ?ㅽ뻾 媛?ν븳 Spring Boot ?좏뵆由ъ??댁뀡?대ʼn, MCP Streamable HTTP? REST ?ㅽ뻾 API瑜??④퍡 ?쒓났?⑸땲??
|
||||
|
||||
이 저장소에는 DATMS(Gateway) 애플리케이션이 포함되어 있지 않습니다. DASMT는 Gateway로 Tool을 push 등록하지 않으며, 각 Pod가 `GET /tool-manifest`를 제공하면 DATMS가 이 Manifest를 pull하여 Tool 목록을 구성합니다. Tool 조회와 직접 실행은 각 Pod에서도 자체적으로 처리합니다.
|
||||
????μ냼?먮뒗 DATMS(Gateway) ?좏뵆由ъ??댁뀡???ы븿?섏뼱 ?덉? ?딆뒿?덈떎. DATMT??Gateway濡?Tool??push ?깅줉?섏? ?딆쑝硫? 媛?Pod媛 `GET /tool-manifest`瑜??쒓났?섎㈃ DATMS媛 ??Manifest瑜?pull?섏뿬 Tool 紐⑸줉??援ъ꽦?⑸땲?? Tool 議고쉶? 吏곸젒 ?ㅽ뻾? 媛?Pod?먯꽌???먯껜?곸쑝濡?泥섎━?⑸땲??
|
||||
|
||||
## 기술 기준
|
||||
## 湲곗닠 湲곗?
|
||||
|
||||
- Java 21
|
||||
- Gradle Wrapper 8.14.3
|
||||
@@ -15,86 +15,86 @@
|
||||
- MapStruct, MyBatis, Redis, Kafka, Resilience4j
|
||||
- JUnit 5
|
||||
|
||||
## 모듈 구성
|
||||
## 紐⑤뱢 援ъ꽦
|
||||
|
||||
| 모듈 | 역할 | 기본 포트 | Tool 수 |
|
||||
| 紐⑤뱢 | ??븷 | 湲곕낯 ?ы듃 | 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 |
|
||||
| `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 |
|
||||
|
||||
업무 모듈에는 총 156개의 `@McpTool` 선언이 있습니다. 현재 업무 모듈의 `src/main/resources`에는 별도 `tool-definitions` YAML이 없으며, Tool 메타데이터는 어노테이션과 `@GrowToolHint`를 기준으로 생성됩니다.
|
||||
?낅Т 紐⑤뱢?먮뒗 珥?156媛쒖쓽 `@McpTool` ?좎뼵???덉뒿?덈떎. ?꾩옱 ?낅Т 紐⑤뱢??`src/main/resources`?먮뒗 蹂꾨룄 `tool-definitions` YAML???놁쑝硫? Tool 硫뷀??곗씠?곕뒗 ?대끂?뚯씠?섍낵 `@GrowToolHint`瑜?湲곗??쇰줈 ?앹꽦?⑸땲??
|
||||
|
||||
현재 업무 구현은 개발·연동 검증 단계입니다. `dat-was-sal`의 `cmm_claim_search`와 `cmm_memo_retriever`는 각각 MCI와 HTTP Client 흐름을 사용하며, 나머지 Tool은 외부 시스템을 변경하지 않는 모의 응답을 중심으로 구현되어 있습니다.
|
||||
?꾩옱 ?낅Т 援ы쁽? 媛쒕컻쨌?곕룞 寃利??④퀎?낅땲?? `dat-was-sal`??`cmm_claim_search`? `cmm_memo_retriever`??媛곴컖 MCI? HTTP Client ?먮쫫???ъ슜?섎ʼn, ?섎㉧吏 Tool? ?몃? ?쒖뒪?쒖쓣 蹂寃쏀븯吏 ?딅뒗 紐⑥쓽 ?묐떟??以묒떖?쇰줈 援ы쁽?섏뼱 ?덉뒿?덈떎.
|
||||
|
||||
## 처리 구조
|
||||
## 泥섎━ 援ъ“
|
||||
|
||||
```text
|
||||
MCP Client 또는 REST Client
|
||||
├─ MCP Streamable HTTP: /mcp
|
||||
└─ REST: POST /mcp/{toolName}
|
||||
│
|
||||
▼
|
||||
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 응답
|
||||
?쒋? 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 ?묐떟
|
||||
```
|
||||
|
||||
애플리케이션 시작 시 다음 순서로 Tool이 준비됩니다.
|
||||
?좏뵆由ъ??댁뀡 ?쒖옉 ???ㅼ쓬 ?쒖꽌濡?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`를 통해 실행됩니다.
|
||||
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`瑜??듯빐 ?ㅽ뻾?⑸땲??
|
||||
|
||||
DASMT 내부에는 `/registry/register`, `/registry/deregister` 호출이나 주기적인 Gateway heartbeat 전송이 없습니다. `ToolRegistryHeartbeatSender`라는 클래스명은 호환성을 위해 남아 있지만 현재 역할은 로컬 Tool 스캔과 메타데이터 생성뿐입니다.
|
||||
DATMT ?대??먮뒗 `/registry/register`, `/registry/deregister` ?몄텧?대굹 二쇨린?곸씤 Gateway heartbeat ?꾩넚???놁뒿?덈떎. `ToolRegistryHeartbeatSender`?쇰뒗 ?대옒?ㅻ챸? ?명솚?깆쓣 ?꾪빐 ?⑥븘 ?덉?留??꾩옱 ??븷? 濡쒖뺄 Tool ?ㅼ틪怨?硫뷀??곗씠???앹꽦肉먯엯?덈떎.
|
||||
|
||||
### DATMS 연동 방식
|
||||
### DATMS ?곕룞 諛⑹떇
|
||||
|
||||
```text
|
||||
DATMS
|
||||
└─ GET {DASMT Pod URL}/tool-manifest
|
||||
└─ bundleId + revision + tools[]
|
||||
└─ 각 Tool endpoint: {Pod URL}/mcp/{toolName}
|
||||
?붴? 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와 DASMT가 반환하는 `bundleId`가 일치해야 같은 Tool bundle로 관리됩니다.
|
||||
- `mcp.manifest.name-prefix`가 비어 있지 않으면 모든 Tool 이름이 해당 prefix로 시작해야 합니다.
|
||||
- Manifest 내용이 바뀌면 `revision`이 증가하며, `If-None-Match`가 일치하면 `304 Not Modified`를 반환합니다.
|
||||
- `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`??利앷??섎ʼn, `If-None-Match`媛 ?쇱튂?섎㈃ `304 Not Modified`瑜?諛섑솚?⑸땲??
|
||||
|
||||
## 제공 API
|
||||
## ?쒓났 API
|
||||
|
||||
각 업무 Pod가 동일한 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}` |
|
||||
| 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`를 반환합니다.
|
||||
`GET /tool-manifest`??`If-None-Match` ?붿껌 ?ㅻ뜑瑜?吏?먰빀?덈떎. Manifest媛 蹂寃쎈릺吏 ?딆븯?쇰㈃ `304 Not Modified`瑜?諛섑솚?⑸땲??
|
||||
|
||||
### REST 실행 예시
|
||||
### REST ?ㅽ뻾 ?덉떆
|
||||
|
||||
다음은 SYS Pod의 시스템 상태 Tool 호출 예시입니다.
|
||||
?ㅼ쓬? SYS Pod???쒖뒪???곹깭 Tool ?몄텧 ?덉떆?낅땲??
|
||||
|
||||
```powershell
|
||||
$headers = @{
|
||||
@@ -104,7 +104,7 @@ $headers = @{
|
||||
'X-Request-Id' = 'request-local-001'
|
||||
'X-Request-Time' = '2026-08-25T12:34:56+09:00'
|
||||
'X-Vrtl-Praf-No' = 'V100001'
|
||||
'X-App-Code' = 'DASMT'
|
||||
'X-App-Code' = 'DATMT'
|
||||
'X-Project-Code' = 'AXHUB'
|
||||
'X-User-Ip' = '10.0.0.10'
|
||||
'X-Caller-Ip' = '10.0.0.20'
|
||||
@@ -119,53 +119,53 @@ Invoke-RestMethod `
|
||||
-Uri 'http://localhost:8086/mcp/iam_system_status' `
|
||||
-Headers $headers `
|
||||
-ContentType 'application/json' `
|
||||
-Body '{"environment":"개발"}'
|
||||
-Body '{"environment":"媛쒕컻"}'
|
||||
```
|
||||
|
||||
### 요청 헤더 계약
|
||||
### ?붿껌 ?ㅻ뜑 怨꾩빟
|
||||
|
||||
DATMS가 DASMT Tool Service를 호출할 때 사용하는 헤더는 다음과 같습니다. HTTP 헤더 이름은 대소문자를 구분하지 않지만, 문서와 구현에서는 아래 표기를 기준으로 사용합니다.
|
||||
DATMS媛 DATMT Tool Service瑜??몄텧?????ъ슜?섎뒗 ?ㅻ뜑???ㅼ쓬怨?媛숈뒿?덈떎. HTTP ?ㅻ뜑 ?대쫫? ??뚮Ц?먮? 援щ텇?섏? ?딆?留? 臾몄꽌? 援ы쁽?먯꽌???꾨옒 ?쒓린瑜?湲곗??쇰줈 ?ъ슜?⑸땲??
|
||||
|
||||
| 헤더 | 필수 여부 | 용도 | 전달 동작 |
|
||||
| ?ㅻ뜑 | ?꾩닔 ?щ? | ?⑸룄 | ?꾨떖 ?숈옉 |
|
||||
|---|---|---|---|
|
||||
| `X-Tool-Server-API-Key` | 인증 설정 시 필수 | DATMS와 DASMT 사이의 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 호출로 전달 |
|
||||
| `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`이 설정됩니다.
|
||||
`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` 헤더는 지원하지 않습니다.
|
||||
湲곗〈 `guid`, `employee-no`, `virtual-employee-no` ?ㅻ뜑??吏?먰븯吏 ?딆뒿?덈떎.
|
||||
|
||||
주요 실행 응답은 다음과 같습니다.
|
||||
二쇱슂 ?ㅽ뻾 ?묐떟? ?ㅼ쓬怨?媛숈뒿?덈떎.
|
||||
|
||||
| HTTP 상태 | 코드 | 의미 |
|
||||
| HTTP ?곹깭 | 肄붾뱶 | ?섎? |
|
||||
|---:|---|---|
|
||||
| 200 | - | Tool 실행 성공 |
|
||||
| 404 | `TOOL_NOT_FOUND` | 요청한 Tool 이름이 없음 |
|
||||
| 422 | `INVALID_PARAM` | 요청이 입력 Schema와 일치하지 않음 |
|
||||
| 500 | `INVALID_TOOL_RESPONSE` | 결과가 출력 Schema와 일치하지 않음 |
|
||||
| 502 | `TOOL_ERROR` | Tool 실행 중 예외 발생 |
|
||||
| 200 | - | Tool ?ㅽ뻾 ?깃났 |
|
||||
| 404 | `TOOL_NOT_FOUND` | ?붿껌??Tool ?대쫫???놁쓬 |
|
||||
| 422 | `INVALID_PARAM` | ?붿껌???낅젰 Schema? ?쇱튂?섏? ?딆쓬 |
|
||||
| 500 | `INVALID_TOOL_RESPONSE` | 寃곌낵媛 異쒕젰 Schema? ?쇱튂?섏? ?딆쓬 |
|
||||
| 502 | `TOOL_ERROR` | Tool ?ㅽ뻾 以??덉쇅 諛쒖깮 |
|
||||
|
||||
## Tool 구현 방식
|
||||
## Tool 援ы쁽 諛⑹떇
|
||||
|
||||
호출 가능한 메서드는 Spring AI Community의 `@McpTool`로 선언합니다. 프로젝트 고유 실행·표시 정보는 `@GrowToolHint`로 보완합니다.
|
||||
?몄텧 媛?ν븳 硫붿꽌?쒕뒗 Spring AI Community??`@McpTool`濡??좎뼵?⑸땲?? ?꾨줈?앺듃 怨좎쑀 ?ㅽ뻾쨌?쒖떆 ?뺣낫??`@GrowToolHint`濡?蹂댁셿?⑸땲??
|
||||
|
||||
```java
|
||||
@McpTool(
|
||||
name = "iam_system_status",
|
||||
title = "시스템 상태 조회",
|
||||
description = "모의 시스템 상태 정보를 조회합니다.",
|
||||
title = "?쒖뒪???곹깭 議고쉶",
|
||||
description = "紐⑥쓽 ?쒖뒪???곹깭 ?뺣낫瑜?議고쉶?⑸땲??",
|
||||
annotations = @McpTool.McpAnnotations(openWorldHint = false)
|
||||
)
|
||||
@GrowToolHint(
|
||||
@@ -176,76 +176,76 @@ DATMS가 DASMT Tool Service를 호출할 때 사용하는 헤더는 다음과
|
||||
SystemStatusResponse getSystemStatus(SystemStatusRequest request);
|
||||
```
|
||||
|
||||
`@McpTool.name`은 다음 형식을 사용합니다.
|
||||
`@McpTool.name`? ?ㅼ쓬 ?뺤떇???ъ슜?⑸땲??
|
||||
|
||||
```text
|
||||
^[a-z][a-z0-9_]{2,63}$
|
||||
```
|
||||
|
||||
예: `cmm_claim_search`, `crm_customer_detail`, `iam_system_status`
|
||||
?? `cmm_claim_search`, `crm_customer_detail`, `iam_system_status`
|
||||
|
||||
이름 중복과 형식은 `validateMcpToolNames` Gradle 작업으로 검사합니다. 현재 루트 `build.gradle`에는 `validateToolSchemaV17` Gradle 작업이 등록되어 있지 않으며, 모든 `bootJar` 작업은 `validateMcpToolNames`에만 의존합니다.
|
||||
?대쫫 以묐났怨??뺤떇? `validateMcpToolNames` Gradle ?묒뾽?쇰줈 寃?ы빀?덈떎. ?꾩옱 猷⑦듃 `build.gradle`?먮뒗 `validateToolSchemaV17` Gradle ?묒뾽???깅줉?섏뼱 ?덉? ?딆쑝硫? 紐⑤뱺 `bootJar` ?묒뾽? `validateMcpToolNames`?먮쭔 ?섏〈?⑸땲??
|
||||
|
||||
## Tool YAML 정의
|
||||
## Tool YAML ?뺤쓽
|
||||
|
||||
공통 라이브러리는 Tool 메타데이터를 보강하기 위한 선택적 YAML 정의를 지원합니다. YAML 정의를 추가할 경우 업무 모듈의 다음 경로를 사용합니다.
|
||||
怨듯넻 ?쇱씠釉뚮윭由щ뒗 Tool 硫뷀??곗씠?곕? 蹂닿컯?섍린 ?꾪븳 ?좏깮??YAML ?뺤쓽瑜?吏?먰빀?덈떎. YAML ?뺤쓽瑜?異붽???寃쎌슦 ?낅Т 紐⑤뱢???ㅼ쓬 寃쎈줈瑜??ъ슜?⑸땲??
|
||||
|
||||
```text
|
||||
dat-was-*/src/main/resources/tool-definitions/{category}/{tool-name}.yml
|
||||
```
|
||||
|
||||
현재 업무 모듈에는 이 경로의 YAML 정의가 없습니다. 따라서 실행 시 메타데이터는 `@McpTool`, `@GrowToolHint`, DTO Schema를 기준으로 구성됩니다. YAML 정의를 도입하는 경우 V17 주요 필수 항목은 다음과 같습니다.
|
||||
?꾩옱 ?낅Т 紐⑤뱢?먮뒗 ??寃쎈줈??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`
|
||||
- 3~10媛쒖쓽 `example_queries`
|
||||
- `read_only`, `destructive`, `idempotent`
|
||||
- `parameters_schema.type: object`
|
||||
- `parameters_schema.additionalProperties: false`
|
||||
- 각 입력 property의 `description`
|
||||
- 媛??낅젰 property??`description`
|
||||
|
||||
입력·출력 Schema는 `ToolSchemaResolver`가 어노테이션의 Schema 리소스와 인라인 Schema, DTO에서 생성한 Schema를 해석합니다. YAML 정의가 있으면 `ToolRegistryHeartbeatSender`가 `parameters_schema`와 `output_schema`를 병합하여 최종 메타데이터를 만듭니다.
|
||||
?낅젰쨌異쒕젰 Schema??`ToolSchemaResolver`媛 ?대끂?뚯씠?섏쓽 Schema 由ъ냼?ㅼ? ?몃씪??Schema, DTO?먯꽌 ?앹꽦??Schema瑜??댁꽍?⑸땲?? YAML ?뺤쓽媛 ?덉쑝硫?`ToolRegistryHeartbeatSender`媛 `parameters_schema`? `output_schema`瑜?蹂묓빀?섏뿬 理쒖쥌 硫뷀??곗씠?곕? 留뚮벊?덈떎.
|
||||
|
||||
실행 시 Schema 검증은 MCP Java SDK의 `DefaultJsonSchemaValidator`를 사용하며 JSON Schema 2020-12 기준으로 처리합니다. 입력 불일치는 `422 INVALID_PARAM`, 출력 불일치는 `500 INVALID_TOOL_RESPONSE`로 반환됩니다. 단, Schema 해석 또는 검증기 자체에서 예외가 발생하면 현재 구현은 오류를 로그에 기록하고 해당 검증을 건너뜁니다.
|
||||
?ㅽ뻾 ??Schema 寃利앹? MCP Java SDK??`DefaultJsonSchemaValidator`瑜??ъ슜?섎ʼn JSON Schema 2020-12 湲곗??쇰줈 泥섎━?⑸땲?? ?낅젰 遺덉씪移섎뒗 `422 INVALID_PARAM`, 異쒕젰 遺덉씪移섎뒗 `500 INVALID_TOOL_RESPONSE`濡?諛섑솚?⑸땲?? ?? Schema ?댁꽍 ?먮뒗 寃利앷린 ?먯껜?먯꽌 ?덉쇅媛 諛쒖깮?섎㈃ ?꾩옱 援ы쁽? ?ㅻ쪟瑜?濡쒓렇??湲곕줉?섍퀬 ?대떦 寃利앹쓣 嫄대꼫?곷땲??
|
||||
|
||||
## 로컬 실행
|
||||
## 濡쒖뺄 ?ㅽ뻾
|
||||
|
||||
### 사전 조건
|
||||
### ?ъ쟾 議곌굔
|
||||
|
||||
- JDK 21
|
||||
- 프로젝트에 포함된 Gradle Wrapper
|
||||
- Redis 또는 외부 연동이 필요한 경우 Docker
|
||||
- ?꾨줈?앺듃???ы븿??Gradle Wrapper
|
||||
- Redis ?먮뒗 ?몃? ?곕룞???꾩슂??寃쎌슦 Docker
|
||||
|
||||
기본 활성 프로필은 `local`입니다. 로컬 프로필은 H2 메모리 DB와 P6Spy를 사용합니다.
|
||||
湲곕낯 ?쒖꽦 ?꾨줈?꾩? `local`?낅땲?? 濡쒖뺄 ?꾨줈?꾩? H2 硫붾え由?DB? P6Spy瑜??ъ슜?⑸땲??
|
||||
|
||||
```powershell
|
||||
$env:SPRING_PROFILES_ACTIVE = 'local'
|
||||
$env:TOOL_SERVER_API_KEY = 'tool-server-key'
|
||||
```
|
||||
|
||||
`TOOL_SERVER_API_KEY`를 지정하지 않으면 현재 개발 기본값인 `tool-server-key`가 사용됩니다. 운영 환경에서는 기본값을 사용하지 말고 DATMS의 Tool Server API Key와 동일한 별도 Secret을 주입해야 합니다.
|
||||
`TOOL_SERVER_API_KEY`瑜?吏?뺥븯吏 ?딆쑝硫??꾩옱 媛쒕컻 湲곕낯媛믪씤 `tool-server-key`媛 ?ъ슜?⑸땲?? ?댁쁺 ?섍꼍?먯꽌??湲곕낯媛믪쓣 ?ъ슜?섏? 留먭퀬 DATMS??Tool Server API Key? ?숈씪??蹂꾨룄 Secret??二쇱엯?댁빞 ?⑸땲??
|
||||
|
||||
각 Pod는 별도 터미널에서 실행합니다.
|
||||
媛?Pod??蹂꾨룄 ?곕??먯뿉???ㅽ뻾?⑸땲??
|
||||
|
||||
```powershell
|
||||
# 영업 Tool Pod
|
||||
# ?곸뾽 Tool Pod
|
||||
$env:AXHUB_TOOL_URL = 'http://localhost:8082'
|
||||
.\gradlew.bat :dat-was-sal:bootRun
|
||||
|
||||
# 고객 Tool Pod
|
||||
# 怨좉컼 Tool Pod
|
||||
$env:AXHUB_TOOL_URL = 'http://localhost:8084'
|
||||
.\gradlew.bat :dat-was-cus:bootRun
|
||||
|
||||
# 상품 Tool Pod
|
||||
# ?곹뭹 Tool Pod
|
||||
$env:AXHUB_TOOL_URL = 'http://localhost:8085'
|
||||
.\gradlew.bat :dat-was-pro:bootRun
|
||||
|
||||
# 시스템 Tool Pod
|
||||
# ?쒖뒪??Tool Pod
|
||||
$env:AXHUB_TOOL_URL = 'http://localhost:8086'
|
||||
.\gradlew.bat :dat-was-sys:bootRun
|
||||
```
|
||||
|
||||
실행 후 SYS Pod 기준 확인 URL은 다음과 같습니다.
|
||||
?ㅽ뻾 ??SYS Pod 湲곗? ?뺤씤 URL? ?ㅼ쓬怨?媛숈뒿?덈떎.
|
||||
|
||||
```text
|
||||
http://localhost:8086/mcp/api/v1/tools/local
|
||||
@@ -253,40 +253,40 @@ 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. 현재 DASMT의 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 대상 포트 | 프로필별 설정 |
|
||||
| `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.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 인증 계층을 추가해야 합니다.
|
||||
諛섎㈃ `/mcp`? `/mcp/message`??蹂꾨룄 Servlet?쇰줈 ?깅줉?섏뼱 Spring MVC `HandlerInterceptor`瑜??듦낵?섏? ?딆뒿?덈떎. ?꾩옱 援ы쁽留뚯쑝濡쒕뒗 ????MCP Streamable HTTP 寃쎈줈??`ApiKeyInterceptor` ?몄쬆???곸슜?섏? ?딆쑝誘濡? ?댁쁺 諛고룷 ??Servlet Filter ?먮뒗 ?꾩슜 MCP ?몄쬆 怨꾩링??異붽??댁빞 ?⑸땲??
|
||||
|
||||
## Scaffold
|
||||
|
||||
공통 라이브러리는 새 Tool과 새 Tool Pod를 만드는 두 개의 Java CLI를 제공합니다. 두 클래스의 `main` 메서드를 IDE에서 실행하거나 필요한 인자를 전달해 실행할 수 있습니다.
|
||||
怨듯넻 ?쇱씠釉뚮윭由щ뒗 ??Tool怨???Tool Pod瑜?留뚮뱶????媛쒖쓽 Java CLI瑜??쒓났?⑸땲?? ???대옒?ㅼ쓽 `main` 硫붿꽌?쒕? IDE?먯꽌 ?ㅽ뻾?섍굅???꾩슂???몄옄瑜??꾨떖???ㅽ뻾?????덉뒿?덈떎.
|
||||
|
||||
| Scaffolder | 역할 | 주요 생성·수정 대상 |
|
||||
| Scaffolder | ??븷 | 二쇱슂 ?앹꽦쨌?섏젙 ???|
|
||||
|---|---|---|
|
||||
| `ToolScaffolder` | 기존 Pod에 Tool 구현 추가 | UseCase, DTO, Converter, Mock 응답, V17 Tool YAML, 연동 설정 |
|
||||
| `PodScaffolder` | 새 실행 Pod 모듈 추가 | 모듈 디렉터리, `build.gradle`, Dockerfile, Application 클래스, 프로필 설정, `settings.gradle`, `docker-compose.yml` |
|
||||
| `ToolScaffolder` | 湲곗〈 Pod??Tool 援ы쁽 異붽? | UseCase, DTO, Converter, Mock ?묐떟, V17 Tool YAML, ?곕룞 ?ㅼ젙 |
|
||||
| `PodScaffolder` | ???ㅽ뻾 Pod 紐⑤뱢 異붽? | 紐⑤뱢 ?붾젆?곕━, `build.gradle`, Dockerfile, Application ?대옒?? ?꾨줈???ㅼ젙, `settings.gradle`, `docker-compose.yml` |
|
||||
|
||||
`PodScaffolder`의 인자 순서는 다음과 같습니다.
|
||||
`PodScaffolder`???몄옄 ?쒖꽌???ㅼ쓬怨?媛숈뒿?덈떎.
|
||||
|
||||
```text
|
||||
PodScaffolder <module-name> <port> [author] [yyyy.MM.dd]
|
||||
```
|
||||
|
||||
예를 들어 `payment 8099`를 입력하면 `dat-was-payment` 모듈을 생성합니다. 생성되는 `application.yml`에는 다음 계약이 포함됩니다.
|
||||
?덈? ?ㅼ뼱 `payment 8099`瑜??낅젰?섎㈃ `dat-was-payment` 紐⑤뱢???앹꽦?⑸땲?? ?앹꽦?섎뒗 `application.yml`?먮뒗 ?ㅼ쓬 怨꾩빟???ы븿?⑸땲??
|
||||
|
||||
```yaml
|
||||
mcp:
|
||||
@@ -297,40 +297,40 @@ mcp:
|
||||
api-key: ${TOOL_SERVER_API_KEY:tool-server-key}
|
||||
```
|
||||
|
||||
생성되는 Compose 서비스에도 `TOOL_SERVER_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 항목을 별도 검증합니다.
|
||||
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 이름·중복 검사
|
||||
# Tool ?대쫫쨌以묐났 寃??
|
||||
.\gradlew.bat validateMcpToolNames
|
||||
|
||||
# 검증, 테스트, 패키징
|
||||
# 寃利? ?뚯뒪?? ?⑦궎吏?
|
||||
.\gradlew.bat clean build
|
||||
```
|
||||
|
||||
### 실행 전 확인
|
||||
### ?ㅽ뻾 ???뺤씤
|
||||
|
||||
테스트 수와 성공 여부는 소스 변경에 따라 달라지므로 고정된 수치를 문서화하지 않습니다. 배포 전 현재 작업 트리에서 다음 명령을 실행해 확인합니다.
|
||||
?뚯뒪???섏? ?깃났 ?щ????뚯뒪 蹂寃쎌뿉 ?곕씪 ?щ씪吏誘濡?怨좎젙???섏튂瑜?臾몄꽌?뷀븯吏 ?딆뒿?덈떎. 諛고룷 ???꾩옱 ?묒뾽 ?몃━?먯꽌 ?ㅼ쓬 紐낅졊???ㅽ뻾???뺤씤?⑸땲??
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat test validateMcpToolNames
|
||||
@@ -338,138 +338,139 @@ mcp:
|
||||
|
||||
## Docker Compose
|
||||
|
||||
`docker-compose.yml`은 DASMT의 네 업무 Pod만 정의합니다.
|
||||
`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를 실행합니다.
|
||||
紐⑤뱢 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 파일의 용도와 현재 주의점은 다음과 같습니다.
|
||||
Compose ?뚯씪???⑸룄? ?꾩옱 二쇱쓽?먯? ?ㅼ쓬怨?媛숈뒿?덈떎.
|
||||
|
||||
| 파일 | 용도 | 현재 소스 기준 주의점 |
|
||||
| ?뚯씪 | ?⑸룄 | ?꾩옱 ?뚯뒪 湲곗? 二쇱쓽??|
|
||||
|---|---|---|
|
||||
| `docker-compose.yml` | DASMT 네 Pod 단독 실행 | `gateway` 서비스가 없지만 push 등록이 제거되어 DASMT 시작에는 필요하지 않음 |
|
||||
| `docker-compose.local.yml` | DATMS와 DASMT의 로컬 통합 구성 | 두 저장소가 같은 상위 디렉터리에 있는 구조를 가정 |
|
||||
| `docker-compose.prod.yml` | DATMS와 DASMT의 개발 프로필 기반 OCI 구성 | 저장소의 runner 등록 토큰을 운영 Secret으로 분리해야 함 |
|
||||
| `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-dasmt`입니다. 현재 파일 위치에서 사용할 때는 context 기준을 두 저장소의 상위 디렉터리로 맞춰야 합니다.
|
||||
`docker-compose.local.yml`怨?`docker-compose.prod.yml`??build context??媛곴컖 `./dat-was-DATMS`, `./dat-was-datmt`?낅땲?? ?꾩옱 ?뚯씪 ?꾩튂?먯꽌 ?ъ슜???뚮뒗 context 湲곗???????μ냼???곸쐞 ?붾젆?곕━濡?留욎떠???⑸땲??
|
||||
|
||||
```powershell
|
||||
# DASMT 저장소 디렉터리에서 실행
|
||||
# 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할 수 있어야 합니다.
|
||||
?꾩옱 湲곗〈 ??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 (Kubernetes) 諛고룷
|
||||
|
||||
OpenShift 개발 환경용 Kustomize 매니페스트는 [`k8s/`](k8s/)에 있습니다. 대상은 DASMT의 네 Tool Pod뿐이며 DATMS(Gateway)의 Deployment·Service·Route는 이 저장소에서 만들지 않습니다.
|
||||
OpenShift 媛쒕컻 ?섍꼍??Kustomize 留ㅻ땲?섏뒪?몃뒗 [`k8s/`](k8s/)???덉뒿?덈떎. ??곸? DATMT????Tool Pod肉먯씠硫?DATMS(Gateway)??Deployment쨌Service쨌Route??????μ냼?먯꽌 留뚮뱾吏 ?딆뒿?덈떎.
|
||||
|
||||
```text
|
||||
k8s/
|
||||
├─ base/ # 네 Pod 공통 ConfigMap, Service, Deployment
|
||||
└─ overlays/dev/ # 개발 namespace, Registry 이미지 경로와 tag
|
||||
?쒋? base/ # ??Pod 怨듯넻 ConfigMap, Service, Deployment
|
||||
?붴? overlays/dev/ # 媛쒕컻 namespace, Registry ?대?吏 寃쎈줈? tag
|
||||
```
|
||||
|
||||
### 배포 구조
|
||||
### 諛고룷 援ъ“
|
||||
|
||||
각 Tool Pod는 Deployment 1개와 외부에 노출되지 않는 `ClusterIP` Service 1개를 사용합니다. OpenShift Route와 LoadBalancer Service는 생성하지 않으며, Gateway가 클러스터 내부 DNS로 호출합니다.
|
||||
媛?Tool Pod??Deployment 1媛쒖? ?몃????몄텧?섏? ?딅뒗 `ClusterIP` Service 1媛쒕? ?ъ슜?⑸땲?? OpenShift Route? LoadBalancer Service???앹꽦?섏? ?딆쑝硫? Gateway媛 ?대윭?ㅽ꽣 ?대? DNS濡??몄텧?⑸땲??
|
||||
|
||||
| Service | Pod 포트 | Gateway 호출 주소 |
|
||||
| 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-dasmt-dev.svc`와 같은 FQDN을 사용하고, NetworkPolicy에서 Gateway namespace의 ingress를 별도로 허용해야 합니다.
|
||||
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 값
|
||||
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 媛?
|
||||
|
||||
`dasmt-runtime-secrets` Secret은 Git에 저장하지 않고 OpenShift namespace에서 별도로 생성합니다. 최소한 `TOOL_SERVER_API_KEY`는 DATMS가 전달하는 `X-Tool-Server-API-Key`와 같은 값이어야 합니다. DB 계정·비밀번호, API Key, 인증서 비밀번호 등도 이 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-dasmt-dev create secret generic dasmt-runtime-secrets `
|
||||
# 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가 시작하지 않을 수 있습니다.
|
||||
Secret???놁쑝硫?媛?Deployment??`envFrom.secretRef`瑜??댁꽍?????놁뼱 Pod媛 ?쒖옉?섏? ?딆쓣 ???덉뒿?덈떎.
|
||||
|
||||
### 이미지 빌드와 배포
|
||||
### ?대?吏 鍮뚮뱶? 諛고룷
|
||||
|
||||
모듈 Dockerfile은 미리 생성된 Boot JAR를 복사하므로, 이미지를 만들기 전에 네 모듈의 JAR를 빌드합니다. 개발망에서는 JDK/JRE 베이스 이미지와 Gradle/Maven 의존성을 내부 Registry·Nexus에서 사용할 수 있어야 합니다.
|
||||
紐⑤뱢 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 선택 후
|
||||
# OpenShift 濡쒓렇??諛?project ?좏깮 ??
|
||||
oc kustomize k8s/overlays/dev
|
||||
oc apply -k k8s/overlays/dev
|
||||
oc get deployment,pod,svc -n axhub-dasmt-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로 변경하는 것을 권장합니다.
|
||||
?곸슜 ?꾩뿉??`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)를 참고합니다.
|
||||
?곸꽭??紐낅졊怨?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와 로그 마스킹 정책을 검토해야 합니다.
|
||||
- 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 ?몄쬆 ??곸씠 ?꾨떃?덈떎. ?대?留씲텶ngress ?뺤콉 ?먮뒗 蹂꾨룄 ?몄쬆???꾩슂?쒖? ?댁쁺 湲곗????뺤씤?댁빞 ?⑸땲??
|
||||
- ???낅Т Pod??湲곕낯 Key??紐⑤몢 `tool-server-key`?낅땲?? ?댁쁺?먯꽌??諛섎뱶??蹂꾨룄 Secret?쇰줈 援먯껜?섍퀬 DATMS??`X-Tool-Server-API-Key` 媛믨낵 ?쇱튂?쒖폒???⑸땲??
|
||||
- ?ㅼ젙???⑥씪 Key? ?ㅼ쨷 Key媛 紐⑤몢 ?놁쓣 ?뚮쭔 ?듬챸 ?붿껌???덉슜?⑸땲??
|
||||
- `mcp.security.tenant-domains`???ㅼ젙 媛앹껜??諛붿씤?⑸릺吏留?Tool蹂??멸????ъ슜?섏? ?딆뒿?덈떎.
|
||||
- `requiresApproval`? 硫뷀??곗씠?곗뿉留?湲곕줉?섎ʼn ?ㅽ뻾 李⑤떒?대굹 ?뱀씤 ?뺤씤 濡쒖쭅? ?놁뒿?덈떎.
|
||||
- ?낅젰쨌異쒕젰 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` |
|
||||
| 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` |
|
||||
| 怨듯넻 鍮뚮뱶 諛?寃利??묒뾽 | `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` |
|
||||
| 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 寃곌낵쨌?붿껌 ?ㅻ뜑 ?꾨떖쨌?ㅻ쪟 怨꾩빟???뺤씤?⑸땲??
|
||||
|
||||
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가 DASMT 설정과 일치하는지 확인합니다.
|
||||
7. REST와 MCP 양쪽에서 동일한 Tool 결과·요청 헤더 전달·오류 계약을 확인합니다.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -16,28 +16,28 @@ import java.util.Scanner;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스입니다.
|
||||
* MCP Tool 肄붾뱶瑜??먮룞 ?앹꽦(Scaffolding)?섎뒗 ?좏떥由ы떚 ?대옒?ㅼ엯?덈떎.
|
||||
*
|
||||
* [실행 방법]
|
||||
* 방법 1. IDE(IntelliJ 등)에서 직접 실행
|
||||
* - ToolScaffolder.java의 main 메서드를 실행합니다.
|
||||
* - 콘솔 질문에 차례대로 값을 입력하면 파일이 생성됩니다.
|
||||
* [?ㅽ뻾 諛⑸쾿]
|
||||
* 諛⑸쾿 1. IDE(IntelliJ ???먯꽌 吏곸젒 ?ㅽ뻾
|
||||
* - ToolScaffolder.java??main 硫붿꽌?쒕? ?ㅽ뻾?⑸땲??
|
||||
* - 肄섏넄 吏덈Ц??李⑤??濡?媛믪쓣 ?낅젰?섎㈃ ?뚯씪???앹꽦?⑸땲??
|
||||
*
|
||||
* 방법 2. 명령줄에서 실행
|
||||
* - 컴파일: javac -encoding UTF-8 dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/ToolScaffolder.java
|
||||
* - 실행: java -cp dat-was-lib/src/main/java io.shinhanlife.dat.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
|
||||
* 諛⑸쾿 2. 紐낅졊以꾩뿉???ㅽ뻾
|
||||
* - 而댄뙆?? javac -encoding UTF-8 dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/ToolScaffolder.java
|
||||
* - ?ㅽ뻾: java -cp dat-was-lib/src/main/java io.shinhanlife.dat.lib.util.ToolScaffolder [?대쫫] [ID] "[?ㅻ챸]" "[洹몃9]" "[?듭떊諛⑹떇]" "[紐⑤뱢紐?"
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dat.lib.util
|
||||
* @className ToolScaffolder
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
* 2026.09.01 0986406 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -237,15 +237,15 @@ public class ToolScaffolder {
|
||||
if (tool.interfaceId() != null && !tool.interfaceId().isBlank()) {
|
||||
methods.append(" mappingId = \"").append(javaText(tool.interfaceId())).append("\",\n");
|
||||
}
|
||||
methods.append(" functionDescription = \"").append(javaText(opts.functionDescription() != null && !opts.functionDescription().isBlank() ? opts.functionDescription() : option(tool.title(), baseName) + " 기능을 수행합니다.")).append("\",\n")
|
||||
.append(" whenToUse = \"").append(javaText(opts.whenToUse() != null && !opts.whenToUse().isBlank() ? opts.whenToUse() : "사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.")).append("\",\n")
|
||||
.append(" whenNotToUse = \"").append(javaText(opts.whenNotToUse() != null && !opts.whenNotToUse().isBlank() ? opts.whenNotToUse() : "정보 변경이나 실행 작업에는 사용하지 않습니다.")).append("\",\n")
|
||||
.append(" ioLimits = \"").append(javaText(opts.ioLimits() != null && !opts.ioLimits().isBlank() ? opts.ioLimits() : "정의된 입력 항목만 허용하며 업무 결과만 반환합니다.")).append("\",\n")
|
||||
.append(" displayDescription = \"").append(javaText(opts.displayDescription() != null && !opts.displayDescription().isBlank() ? opts.displayDescription() : option(tool.title(), baseName) + " 정보를 처리합니다.")).append("\",\n");
|
||||
methods.append(" functionDescription = \"").append(javaText(opts.functionDescription() != null && !opts.functionDescription().isBlank() ? opts.functionDescription() : option(tool.title(), baseName) + " 湲곕뒫???섑뻾?⑸땲??")).append("\",\n")
|
||||
.append(" whenToUse = \"").append(javaText(opts.whenToUse() != null && !opts.whenToUse().isBlank() ? opts.whenToUse() : "?ъ슜?먭? ???낅Т 湲곕뒫???ㅽ뻾 ?먮뒗 議고쉶瑜??붿껌?????ъ슜?⑸땲??")).append("\",\n")
|
||||
.append(" whenNotToUse = \"").append(javaText(opts.whenNotToUse() != null && !opts.whenNotToUse().isBlank() ? opts.whenNotToUse() : "?뺣낫 蹂寃쎌씠???ㅽ뻾 ?묒뾽?먮뒗 ?ъ슜?섏? ?딆뒿?덈떎.")).append("\",\n")
|
||||
.append(" ioLimits = \"").append(javaText(opts.ioLimits() != null && !opts.ioLimits().isBlank() ? opts.ioLimits() : "?뺤쓽???낅젰 ??ぉ留??덉슜?섎ʼn ?낅Т 寃곌낵留?諛섑솚?⑸땲??")).append("\",\n")
|
||||
.append(" displayDescription = \"").append(javaText(opts.displayDescription() != null && !opts.displayDescription().isBlank() ? opts.displayDescription() : option(tool.title(), baseName) + " ?뺣낫瑜?泥섎━?⑸땲??")).append("\",\n");
|
||||
|
||||
List<String> examples = opts.exampleQueries();
|
||||
if (examples == null || examples.isEmpty()) {
|
||||
examples = List.of(option(tool.title(), baseName) + " 정보를 보여줘", option(tool.title(), baseName) + " 확인해줘", "현재 " + option(tool.title(), baseName) + " 알려줘");
|
||||
examples = List.of(option(tool.title(), baseName) + " ?뺣낫瑜?蹂댁뿬以?, option(tool.title(), baseName) + " ?뺤씤?댁쨾", "?꾩옱 " + option(tool.title(), baseName) + " ?뚮젮以?);
|
||||
}
|
||||
methods.append(" exampleQueries = {")
|
||||
.append(examples.stream().map(q -> "\"" + javaText(q) + "\"").collect(Collectors.joining(", ")))
|
||||
@@ -255,7 +255,7 @@ public class ToolScaffolder {
|
||||
|
||||
List<String> tags = opts.tags();
|
||||
if (tags == null || tags.isEmpty()) {
|
||||
tags = List.of(tool.group().toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회");
|
||||
tags = List.of(tool.group().toLowerCase(Locale.ROOT), isMutation ? "泥섎━" : "議고쉶");
|
||||
}
|
||||
methods.append(" tags = {")
|
||||
.append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", ")))
|
||||
@@ -392,15 +392,15 @@ public class ToolScaffolder {
|
||||
if (mappingId != null && !mappingId.isBlank()) {
|
||||
declBuilder.append(" mappingId = \"").append(javaText(mappingId)).append("\",\n");
|
||||
}
|
||||
declBuilder.append(" functionDescription = \"").append(javaText(opts.functionDescription() != null && !opts.functionDescription().isBlank() ? opts.functionDescription() : option(tool.title(), baseName) + " 기능을 수행합니다.")).append("\",\n")
|
||||
.append(" whenToUse = \"").append(javaText(opts.whenToUse() != null && !opts.whenToUse().isBlank() ? opts.whenToUse() : "사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.")).append("\",\n")
|
||||
.append(" whenNotToUse = \"").append(javaText(opts.whenNotToUse() != null && !opts.whenNotToUse().isBlank() ? opts.whenNotToUse() : "정보 변경이나 실행 작업에는 사용하지 않습니다.")).append("\",\n")
|
||||
.append(" ioLimits = \"").append(javaText(opts.ioLimits() != null && !opts.ioLimits().isBlank() ? opts.ioLimits() : "정의된 입력 항목만 허용하며 업무 결과만 반환합니다.")).append("\",\n")
|
||||
.append(" displayDescription = \"").append(javaText(opts.displayDescription() != null && !opts.displayDescription().isBlank() ? opts.displayDescription() : option(tool.title(), baseName) + " 정보를 처리합니다.")).append("\",\n");
|
||||
declBuilder.append(" functionDescription = \"").append(javaText(opts.functionDescription() != null && !opts.functionDescription().isBlank() ? opts.functionDescription() : option(tool.title(), baseName) + " 湲곕뒫???섑뻾?⑸땲??")).append("\",\n")
|
||||
.append(" whenToUse = \"").append(javaText(opts.whenToUse() != null && !opts.whenToUse().isBlank() ? opts.whenToUse() : "?ъ슜?먭? ???낅Т 湲곕뒫???ㅽ뻾 ?먮뒗 議고쉶瑜??붿껌?????ъ슜?⑸땲??")).append("\",\n")
|
||||
.append(" whenNotToUse = \"").append(javaText(opts.whenNotToUse() != null && !opts.whenNotToUse().isBlank() ? opts.whenNotToUse() : "?뺣낫 蹂寃쎌씠???ㅽ뻾 ?묒뾽?먮뒗 ?ъ슜?섏? ?딆뒿?덈떎.")).append("\",\n")
|
||||
.append(" ioLimits = \"").append(javaText(opts.ioLimits() != null && !opts.ioLimits().isBlank() ? opts.ioLimits() : "?뺤쓽???낅젰 ??ぉ留??덉슜?섎ʼn ?낅Т 寃곌낵留?諛섑솚?⑸땲??")).append("\",\n")
|
||||
.append(" displayDescription = \"").append(javaText(opts.displayDescription() != null && !opts.displayDescription().isBlank() ? opts.displayDescription() : option(tool.title(), baseName) + " ?뺣낫瑜?泥섎━?⑸땲??")).append("\",\n");
|
||||
|
||||
List<String> examples = opts.exampleQueries();
|
||||
if (examples == null || examples.isEmpty()) {
|
||||
examples = List.of(option(tool.title(), baseName) + " 정보를 보여줘", option(tool.title(), baseName) + " 확인해줘", "현재 " + option(tool.title(), baseName) + " 알려줘");
|
||||
examples = List.of(option(tool.title(), baseName) + " ?뺣낫瑜?蹂댁뿬以?, option(tool.title(), baseName) + " ?뺤씤?댁쨾", "?꾩옱 " + option(tool.title(), baseName) + " ?뚮젮以?);
|
||||
}
|
||||
declBuilder.append(" exampleQueries = {")
|
||||
.append(examples.stream().map(q -> "\"" + javaText(q) + "\"").collect(Collectors.joining(", ")))
|
||||
@@ -410,7 +410,7 @@ public class ToolScaffolder {
|
||||
|
||||
List<String> tags = opts.tags();
|
||||
if (tags == null || tags.isEmpty()) {
|
||||
tags = List.of(tool.group().toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회");
|
||||
tags = List.of(tool.group().toLowerCase(Locale.ROOT), isMutation ? "泥섎━" : "議고쉶");
|
||||
}
|
||||
declBuilder.append(" tags = {")
|
||||
.append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", ")))
|
||||
@@ -525,8 +525,8 @@ public class ToolScaffolder {
|
||||
public static void main(String[] args) throws IOException {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: ");
|
||||
String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): ");
|
||||
String baseName = getOrAsk(args, 0, scanner, "1. ?앹꽦??Tool??湲곕낯 ?대쫫 (?? ExchangeRate) [?곷Ц PascalCase]: ");
|
||||
String interfaceId = getOrAsk(args, 1, scanner, "2. ?덇굅??API ?명꽣?섏씠??ID (?? EXCH_001): ");
|
||||
String title = getOrAsk(args, 2, scanner, "3. Tool title: ");
|
||||
String description = getOrAsk(args, 3, scanner, "4. Tool description for LLM: ");
|
||||
String group = getOrAsk(args, 4, scanner, "5. Tool category: ");
|
||||
@@ -543,19 +543,19 @@ public class ToolScaffolder {
|
||||
String defaultAuthor = System.getProperty("user.name");
|
||||
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
|
||||
|
||||
String author = getOrAsk(args, 7, scanner, "8. 작성자(Enter 입력 시 '" + defaultAuthor + "'): ");
|
||||
String author = getOrAsk(args, 7, scanner, "8. ?묒꽦??Enter ?낅젰 ??'" + defaultAuthor + "'): ");
|
||||
if (author.trim().isEmpty()) author = defaultAuthor;
|
||||
String createDate = getOrAsk(args, 8, scanner, "9. 작성일(Enter 입력 시 '" + defaultDate + "'): ");
|
||||
String createDate = getOrAsk(args, 8, scanner, "9. ?묒꽦??Enter ?낅젰 ??'" + defaultDate + "'): ");
|
||||
if (createDate.trim().isEmpty()) createDate = defaultDate;
|
||||
|
||||
String useSchemaResourceStr = getOrAsk(args, 9, scanner, "10. input/output JSON Schema 파일 자동 생성 여부 (y/N): ");
|
||||
String useSchemaResourceStr = getOrAsk(args, 9, scanner, "10. input/output JSON Schema ?뚯씪 ?먮룞 ?앹꽦 ?щ? (y/N): ");
|
||||
boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim());
|
||||
|
||||
String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/";
|
||||
String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null;
|
||||
String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-schema.json" : null;
|
||||
String defaultWorkspace = "c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\dat-was-dasmt";
|
||||
String workspace = getOrAsk(args, 10, scanner, "11. 대상 프로젝트 워크스페이스 경로 (default: " + defaultWorkspace + "): ");
|
||||
String defaultWorkspace = "c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\dat-was-datmt";
|
||||
String workspace = getOrAsk(args, 10, scanner, "11. ????꾨줈?앺듃 ?뚰겕?ㅽ럹?댁뒪 寃쎈줈 (default: " + defaultWorkspace + "): ");
|
||||
if (workspace.trim().isEmpty()) {
|
||||
workspace = defaultWorkspace;
|
||||
}
|
||||
@@ -631,7 +631,7 @@ public class ToolScaffolder {
|
||||
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "legacy"));
|
||||
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "converter"));
|
||||
|
||||
// Schema Resource 파일 경로(useSchemaResource=true일 때만 생성)
|
||||
// Schema Resource ?뚯씪 寃쎈줈(useSchemaResource=true???뚮쭔 ?앹꽦)
|
||||
boolean useSchemaResource = (inputSchemaResource != null && !inputSchemaResource.trim().isEmpty()) || (outputSchemaResource != null && !outputSchemaResource.trim().isEmpty());
|
||||
String schemaBaseName = toKebabCase(baseName);
|
||||
String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json";
|
||||
@@ -692,25 +692,25 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.dto
|
||||
* @className %sRequest
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class %sRequest {
|
||||
@McpToolParam(description = "수신자 전화번호", required = true)
|
||||
@McpToolParam(description = "?섏떊???꾪솕踰덊샇", required = true)
|
||||
-(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$", examples = {"010-1234-5678"})
|
||||
private String phoneNumber;
|
||||
|
||||
@McpToolParam(description = "전송할 메시지 내용", required = true)
|
||||
@McpToolParam(description = "?꾩넚??硫붿떆吏 ?댁슜", required = true)
|
||||
private String message;
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
@@ -720,7 +720,7 @@ public class ToolScaffolder {
|
||||
.replaceAll("(?m)^\\s*@McpToolParam\\([^\\r\\n]*\\)\\R", "")
|
||||
.replaceAll("(?m)^\\s*-\\(\\?:[^\\r\\n]*\\R", "")
|
||||
.replace("private String phoneNumber;", "@Schema(example = \"01012345678\")\n private String phoneNumber;")
|
||||
.replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;");
|
||||
.replace("private String message;", "@Schema(example = \"?뚯뒪??硫붿떆吏?낅땲??\")\n private String message;");
|
||||
reqContent = dtoContent(bizPackage + ".dto", baseName + "Request", inputFields, author, createDate, true);
|
||||
writeUtf8(dtoDir.resolve(baseName + "Request.java"), reqContent);
|
||||
writeStructuredFieldTypes(dtoDir, bizPackage + ".dto", baseName + "Request", inputFields);
|
||||
@@ -735,14 +735,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.dto
|
||||
* @className %sResponse
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -775,15 +775,15 @@ public class ToolScaffolder {
|
||||
if (interfaceId != null && !interfaceId.isBlank()) {
|
||||
sb.append(" mappingId = \"").append(interfaceId).append("\",\n");
|
||||
}
|
||||
sb.append(" functionDescription = \"").append(javaText(definitionOptions.functionDescription() != null && !definitionOptions.functionDescription().isBlank() ? definitionOptions.functionDescription() : title + " 기능을 수행합니다.")).append("\",\n");
|
||||
sb.append(" whenToUse = \"").append(javaText(definitionOptions.whenToUse() != null && !definitionOptions.whenToUse().isBlank() ? definitionOptions.whenToUse() : "사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.")).append("\",\n");
|
||||
sb.append(" whenNotToUse = \"").append(javaText(definitionOptions.whenNotToUse() != null && !definitionOptions.whenNotToUse().isBlank() ? definitionOptions.whenNotToUse() : "정보 변경이나 실행 작업에는 사용하지 않습니다.")).append("\",\n");
|
||||
sb.append(" ioLimits = \"").append(javaText(definitionOptions.ioLimits() != null && !definitionOptions.ioLimits().isBlank() ? definitionOptions.ioLimits() : "정의된 입력 항목만 허용하며 업무 결과만 반환합니다.")).append("\",\n");
|
||||
sb.append(" displayDescription = \"").append(javaText(definitionOptions.displayDescription() != null && !definitionOptions.displayDescription().isBlank() ? definitionOptions.displayDescription() : title + " 정보를 처리합니다.")).append("\",\n");
|
||||
sb.append(" functionDescription = \"").append(javaText(definitionOptions.functionDescription() != null && !definitionOptions.functionDescription().isBlank() ? definitionOptions.functionDescription() : title + " 湲곕뒫???섑뻾?⑸땲??")).append("\",\n");
|
||||
sb.append(" whenToUse = \"").append(javaText(definitionOptions.whenToUse() != null && !definitionOptions.whenToUse().isBlank() ? definitionOptions.whenToUse() : "?ъ슜?먭? ???낅Т 湲곕뒫???ㅽ뻾 ?먮뒗 議고쉶瑜??붿껌?????ъ슜?⑸땲??")).append("\",\n");
|
||||
sb.append(" whenNotToUse = \"").append(javaText(definitionOptions.whenNotToUse() != null && !definitionOptions.whenNotToUse().isBlank() ? definitionOptions.whenNotToUse() : "?뺣낫 蹂寃쎌씠???ㅽ뻾 ?묒뾽?먮뒗 ?ъ슜?섏? ?딆뒿?덈떎.")).append("\",\n");
|
||||
sb.append(" ioLimits = \"").append(javaText(definitionOptions.ioLimits() != null && !definitionOptions.ioLimits().isBlank() ? definitionOptions.ioLimits() : "?뺤쓽???낅젰 ??ぉ留??덉슜?섎ʼn ?낅Т 寃곌낵留?諛섑솚?⑸땲??")).append("\",\n");
|
||||
sb.append(" displayDescription = \"").append(javaText(definitionOptions.displayDescription() != null && !definitionOptions.displayDescription().isBlank() ? definitionOptions.displayDescription() : title + " ?뺣낫瑜?泥섎━?⑸땲??")).append("\",\n");
|
||||
|
||||
List<String> examples = definitionOptions.exampleQueries();
|
||||
if (examples == null || examples.isEmpty()) {
|
||||
examples = List.of(title + " 정보를 보여줘", title + " 확인해줘", "현재 " + title + " 알려줘");
|
||||
examples = List.of(title + " ?뺣낫瑜?蹂댁뿬以?, title + " ?뺤씤?댁쨾", "?꾩옱 " + title + " ?뚮젮以?);
|
||||
}
|
||||
sb.append(" exampleQueries = {")
|
||||
.append(examples.stream().map(q -> "\"" + javaText(q) + "\"").collect(Collectors.joining(", ")))
|
||||
@@ -794,7 +794,7 @@ public class ToolScaffolder {
|
||||
|
||||
List<String> tags = definitionOptions.tags();
|
||||
if (tags == null || tags.isEmpty()) {
|
||||
tags = List.of(group.toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회");
|
||||
tags = List.of(group.toLowerCase(Locale.ROOT), isMutation ? "泥섎━" : "議고쉶");
|
||||
}
|
||||
sb.append(" tags = {")
|
||||
.append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", ")))
|
||||
@@ -815,14 +815,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.usecase
|
||||
* @className %sUseCase
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -866,14 +866,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.usecase.impl
|
||||
* @className %sUseCaseImpl
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -887,9 +887,9 @@ public class ToolScaffolder {
|
||||
|
||||
@Override
|
||||
public %sResponse execute(%sRequest req) {
|
||||
log.info("[MCI Tool] {} 요청 수신.", "%s");
|
||||
log.info("[MCI Tool] {} ?붿껌 ?섏떊.", "%s");
|
||||
try {
|
||||
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
|
||||
// MapStruct瑜??댁슜???먮룞 留ㅽ븨 (AI DTO -> MCI DTO)
|
||||
%s_I mciReq = converter.toLegacyRequest(req);
|
||||
|
||||
Transfer<Object> resTransfer = mci.callTo(
|
||||
@@ -905,7 +905,7 @@ public class ToolScaffolder {
|
||||
: "MCI call completed without a response body.");
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||
log.error("[MCI Tool] ?곕룞 以??ㅻ쪟 諛쒖깮: {}", e.getMessage(), e);
|
||||
%sResponse response = new %sResponse();
|
||||
response.setResultCode("ERROR");
|
||||
response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||
@@ -965,14 +965,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.usecase.impl
|
||||
* @className %sUseCaseImpl
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1029,26 +1029,26 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.%s.io
|
||||
* @className %s_I
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %s_I {
|
||||
/**
|
||||
* EAI 시스템이 요구하는 수신자 번호 파라미터명
|
||||
* EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 ?섏떊??踰덊샇 ?뚮씪誘명꽣紐?
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* EAI 시스템이 요구하는 메시지 내용 파라미터명
|
||||
* EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 硫붿떆吏 ?댁슜 ?뚮씪誘명꽣紐?
|
||||
*/
|
||||
private String content;
|
||||
}
|
||||
@@ -1065,14 +1065,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.%s.io
|
||||
* @className %s_O
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1099,14 +1099,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.converter
|
||||
* @className %sConverter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1160,14 +1160,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.%s
|
||||
* @className Mci%sClient
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1225,26 +1225,26 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.legacy
|
||||
* @className %sLegacyRequest
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %sLegacyRequest {
|
||||
/**
|
||||
* EAI 시스템이 요구하는 수신자 번호 파라미터명
|
||||
* EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 ?섏떊??踰덊샇 ?뚮씪誘명꽣紐?
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* EAI 시스템이 요구하는 메시지 내용 파라미터명
|
||||
* EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 硫붿떆吏 ?댁슜 ?뚮씪誘명꽣紐?
|
||||
*/
|
||||
private String content;
|
||||
}
|
||||
@@ -1260,14 +1260,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.legacy
|
||||
* @className %sLegacyResponse
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1293,14 +1293,14 @@ public class ToolScaffolder {
|
||||
/**
|
||||
* @package %s.converter
|
||||
* @className %sConverter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- 媛쒖젙?대젰 ----------
|
||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
* %s %s 理쒖큹?앹꽦
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1340,7 +1340,7 @@ public class ToolScaffolder {
|
||||
log.append("[Legacy Response DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyResponse.java")).append("\n");
|
||||
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
|
||||
}
|
||||
// Schema Resource 파일 생성(useSchemaResource=true일 때)
|
||||
// Schema Resource ?뚯씪 ?앹꽦(useSchemaResource=true????
|
||||
if (useSchemaResource) {
|
||||
Files.createDirectories(schemaDir);
|
||||
String inputSchema = """
|
||||
@@ -1350,7 +1350,7 @@ public class ToolScaffolder {
|
||||
"properties": {
|
||||
"TODO_FIELD": {
|
||||
"type": "string",
|
||||
"description": "TODO: 파라미터 설명을 입력하세요."
|
||||
"description": "TODO: ?뚮씪誘명꽣 ?ㅻ챸???낅젰?섏꽭??"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@@ -1363,12 +1363,12 @@ public class ToolScaffolder {
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "처리 결과 상태 (SUCCESS / FAILURE)",
|
||||
"description": "泥섎━ 寃곌낵 ?곹깭 (SUCCESS / FAILURE)",
|
||||
"enum": ["SUCCESS", "FAILURE"]
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "처리 결과 메시지"
|
||||
"description": "泥섎━ 寃곌낵 硫붿떆吏"
|
||||
}
|
||||
},
|
||||
"required": ["status"]
|
||||
@@ -1412,7 +1412,7 @@ public class ToolScaffolder {
|
||||
log.append("[Test Command] .\\gradlew.bat :").append(moduleName.substring(moduleName.lastIndexOf(java.io.File.separator) + 1)).append(":test --tests \"*").append(baseName).append("UseCaseTest\"\\n");
|
||||
// Tool Definition YML is no longer generated. We use @GrowToolHint instead.
|
||||
// log.append("[V17 Tool Definition] ...\n");
|
||||
log.append("\n Tip: HTTP Tool은 WireMock 실행 후 생성된 mapping URL로 호출을 확인하세요.\n");
|
||||
log.append("\n Tip: HTTP Tool? WireMock ?ㅽ뻾 ???앹꽦??mapping URL濡??몄텧???뺤씤?섏꽭??\n");
|
||||
|
||||
return log.toString();
|
||||
}
|
||||
@@ -1421,7 +1421,7 @@ public class ToolScaffolder {
|
||||
String categoryKey, String interfaceId,
|
||||
List<FieldDefinition> inputFields, boolean mutation) {
|
||||
String safeDescription = description == null || description.isBlank()
|
||||
? title + " 기능을 수행한다." : description;
|
||||
? title + " 湲곕뒫???섑뻾?쒕떎." : description;
|
||||
StringBuilder properties = new StringBuilder();
|
||||
StringBuilder required = new StringBuilder();
|
||||
Set<String> generatedNames = new LinkedHashSet<>();
|
||||
@@ -1448,14 +1448,14 @@ public class ToolScaffolder {
|
||||
category_key: %s
|
||||
description:
|
||||
function: %s
|
||||
when_to_use: 사용자가 이 업무 기능의 실행 또는 조회를 명확히 요청한 경우 사용한다.
|
||||
when_not_to_use: 입력값이 확인되지 않았거나 다른 업무 기능이 더 적합한 경우에는 사용하지 않는다.
|
||||
io_limits: 정의된 입력 항목만 허용하며 응답 DTO에 정의된 업무 결과만 반환한다.
|
||||
when_to_use: ?ъ슜?먭? ???낅Т 湲곕뒫???ㅽ뻾 ?먮뒗 議고쉶瑜?紐낇솗???붿껌??寃쎌슦 ?ъ슜?쒕떎.
|
||||
when_not_to_use: ?낅젰媛믪씠 ?뺤씤?섏? ?딆븯嫄곕굹 ?ㅻⅨ ?낅Т 湲곕뒫?????곹빀??寃쎌슦?먮뒗 ?ъ슜?섏? ?딅뒗??
|
||||
io_limits: ?뺤쓽???낅젰 ??ぉ留??덉슜?섎ʼn ?묐떟 DTO???뺤쓽???낅Т 寃곌낵留?諛섑솚?쒕떎.
|
||||
display_description: %s
|
||||
example_queries:
|
||||
- %s 처리해줘
|
||||
- %s 정보를 확인해줘
|
||||
- %s 업무 결과를 알려줘
|
||||
- %s 泥섎━?댁쨾
|
||||
- %s ?뺣낫瑜??뺤씤?댁쨾
|
||||
- %s ?낅Т 寃곌낵瑜??뚮젮以?
|
||||
read_only: %s
|
||||
destructive: %s
|
||||
idempotent: %s
|
||||
@@ -1477,16 +1477,16 @@ public class ToolScaffolder {
|
||||
List<FieldDefinition> inputFields, boolean mutation,
|
||||
ToolDefinitionOptions options) {
|
||||
String function = option(options == null ? null : options.functionDescription(),
|
||||
option(description, title + " 기능을 수행한다."));
|
||||
option(description, title + " 湲곕뒫???섑뻾?쒕떎."));
|
||||
String whenToUse = option(options == null ? null : options.whenToUse(),
|
||||
"사용자가 해당 업무 기능의 실행 또는 조회를 명확히 요청한 경우 사용한다.");
|
||||
"?ъ슜?먭? ?대떦 ?낅Т 湲곕뒫???ㅽ뻾 ?먮뒗 議고쉶瑜?紐낇솗???붿껌??寃쎌슦 ?ъ슜?쒕떎.");
|
||||
String whenNotToUse = option(options == null ? null : options.whenNotToUse(),
|
||||
"필수 입력값이 확인되지 않았거나 다른 업무 기능이 더 적합한 경우에는 사용하지 않는다.");
|
||||
"?꾩닔 ?낅젰媛믪씠 ?뺤씤?섏? ?딆븯嫄곕굹 ?ㅻⅨ ?낅Т 湲곕뒫?????곹빀??寃쎌슦?먮뒗 ?ъ슜?섏? ?딅뒗??");
|
||||
String ioLimits = option(options == null ? null : options.ioLimits(),
|
||||
"정의된 입력 항목만 허용하며 응답 DTO에 정의된 업무 결과만 반환한다.");
|
||||
"?뺤쓽???낅젰 ??ぉ留??덉슜?섎ʼn ?묐떟 DTO???뺤쓽???낅Т 寃곌낵留?諛섑솚?쒕떎.");
|
||||
String displayDescription = option(options == null ? null : options.displayDescription(), title);
|
||||
List<String> examples = normalizedList(options == null ? null : options.exampleQueries(), List.of(
|
||||
title + " 처리해줘", title + " 정보를 확인해줘", title + " 업무 결과를 알려줘"));
|
||||
title + " 泥섎━?댁쨾", title + " ?뺣낫瑜??뺤씤?댁쨾", title + " ?낅Т 寃곌낵瑜??뚮젮以?));
|
||||
List<String> tags = normalizedList(options == null ? null : options.tags(),
|
||||
List.of(categoryKey.toLowerCase(Locale.ROOT)));
|
||||
String ownerOrg = option(options == null ? null : options.ownerOrg(), "MCP_TOOL");
|
||||
@@ -2008,10 +2008,10 @@ public class ToolScaffolder {
|
||||
private static String richDescription(FieldDefinition field) {
|
||||
String desc = field.description() == null ? "" : field.description().trim();
|
||||
if (field.pattern() != null && !field.pattern().isBlank()) {
|
||||
desc += " (형식: " + field.pattern() + ")";
|
||||
desc += " (?뺤떇: " + field.pattern() + ")";
|
||||
}
|
||||
if (field.examples() != null && !field.examples().isEmpty()) {
|
||||
desc += " (예시: " + String.join(", ", field.examples()) + ")";
|
||||
desc += " (?덉떆: " + String.join(", ", field.examples()) + ")";
|
||||
}
|
||||
return desc.trim();
|
||||
}
|
||||
@@ -2163,3 +2163,4 @@ public class ToolScaffolder {
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -54,37 +54,37 @@
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="dtoDownloadButton" class="primary" type="button">DTO 다운로드</button>
|
||||
<button id="dtoDownloadButton" class="primary" type="button">DTO ?ㅼ슫濡쒕뱶</button>
|
||||
<span class="badge" id="manifestStatus" style="border:1px solid #3f3f46; border-radius:99px; padding:4px 8px; color:#a1a1aa; font-size:11px; font-family:ui-monospace,monospace;">Manifest loading</span>
|
||||
<span class="text-[10px] uppercase tracking-widest px-2 py-1 rounded font-bold" style="background:rgba(59,130,246,0.1); color:#60a5fa; border:1px solid rgba(59,130,246,0.2);">v0.0.1</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<section class="intro"><h1>Schema 기반 Tool 테스트</h1><p>Tool을 선택하고 요청 JSON을 확인한 뒤 실행하세요. 검증한 요청은 브라우저에 저장되며, 저장된 케이스 전체를 한 번에 다시 실행할 수 있습니다.</p></section>
|
||||
<section class="intro"><h1>Schema 湲곕컲 Tool ?뚯뒪??/h1><p>Tool???좏깮?섍퀬 ?붿껌 JSON???뺤씤?????ㅽ뻾?섏꽭?? 寃利앺븳 ?붿껌? 釉뚮씪?곗?????λ릺硫? ??λ맂 耳?댁뒪 ?꾩껜瑜???踰덉뿉 ?ㅼ떆 ?ㅽ뻾?????덉뒿?덈떎.</p></section>
|
||||
<div class="grid">
|
||||
<aside class="stack">
|
||||
<section class="card stack"><h2>1. Tool 선택</h2><div><label for="filter">검색</label><input id="filter" placeholder="이름, 설명으로 검색"></div><div><label for="toolSelect">Tool</label><select id="toolSelect" size="12"></select></div><div class="meta" id="toolMeta"></div><div class="buttons"><button id="sampleButton">Schema 샘플 채우기</button><button id="reloadButton">Manifest 새로고침</button></div></section>
|
||||
<section class="card"><h2>저장된 테스트 케이스</h2><div class="buttons"><button class="primary" id="runAllButton">Run saved cases</button><button class="danger" id="clearCasesButton">전체 삭제</button></div><div id="caseList" class="case-list"></div><div id="runLog" class="run-log"></div></section>
|
||||
<section class="card stack"><h2>1. Tool ?좏깮</h2><div><label for="filter">寃??/label><input id="filter" placeholder="?대쫫, ?ㅻ챸?쇰줈 寃??></div><div><label for="toolSelect">Tool</label><select id="toolSelect" size="12"></select></div><div class="meta" id="toolMeta"></div><div class="buttons"><button id="sampleButton">Schema ?섑뵆 梨꾩슦湲?/button><button id="reloadButton">Manifest ?덈줈怨좎묠</button></div></section>
|
||||
<section class="card"><h2>??λ맂 ?뚯뒪??耳?댁뒪</h2><div class="buttons"><button class="primary" id="runAllButton">Run saved cases</button><button class="danger" id="clearCasesButton">?꾩껜 ??젣</button></div><div id="caseList" class="case-list"></div><div id="runLog" class="run-log"></div></section>
|
||||
</aside>
|
||||
<section class="stack">
|
||||
<section class="card"><h2>2. 요청 JSON</h2><div class="notice">필수값과 형식은 Tool의 inputSchema 기준입니다. MCI·외부 연동 Tool은 업무에 맞는 테스트 데이터를 입력한 후 저장하세요.</div><textarea id="arguments" spellcheck="false" aria-label="요청 JSON"></textarea><div class="buttons" style="margin-top:12px"><button id="saveButton">현재 요청 저장</button><button class="primary" id="executeButton">실행</button></div></section>
|
||||
<section class="card"><h2>3. 실행 결과</h2><div class="meta"><span class="badge" id="httpStatus">대기</span><span class="badge" id="latency">-</span><span class="badge" id="traceId">X-Guid: -</span><span class="badge" id="requestId">X-Request-Id: -</span></div><pre class="result" id="result">Tool을 선택하고 실행하세요.</pre></section>
|
||||
<section class="card"><h2>2. ?붿껌 JSON</h2><div class="notice">?꾩닔媛믨낵 ?뺤떇? Tool??inputSchema 湲곗??낅땲?? MCI쨌?몃? ?곕룞 Tool? ?낅Т??留욌뒗 ?뚯뒪???곗씠?곕? ?낅젰??????ν븯?몄슂.</div><textarea id="arguments" spellcheck="false" aria-label="?붿껌 JSON"></textarea><div class="buttons" style="margin-top:12px"><button id="saveButton">?꾩옱 ?붿껌 ???/button><button class="primary" id="executeButton">?ㅽ뻾</button></div></section>
|
||||
<section class="card"><h2>3. ?ㅽ뻾 寃곌낵</h2><div class="meta"><span class="badge" id="httpStatus">?湲?/span><span class="badge" id="latency">-</span><span class="badge" id="traceId">X-Guid: -</span><span class="badge" id="requestId">X-Request-Id: -</span></div><pre class="result" id="result">Tool???좏깮?섍퀬 ?ㅽ뻾?섏꽭??</pre></section>
|
||||
</section>
|
||||
</div>
|
||||
<p class="footer-note">이 화면은 현재 Tool Pod의 <code>/tool-manifest</code>와 <code>/mcp/{toolName}</code>만 사용합니다. 저장된 케이스는 이 브라우저의 localStorage에만 보관됩니다.</p>
|
||||
<p class="footer-note">???붾㈃? ?꾩옱 Tool Pod??<code>/tool-manifest</code>? <code>/mcp/{toolName}</code>留??ъ슜?⑸땲?? ??λ맂 耳?댁뒪????釉뚮씪?곗???localStorage?먮쭔 蹂닿??⑸땲??</p>
|
||||
</main>
|
||||
<div id="dtoDownloadModal" class="modal-backdrop hidden" role="dialog" aria-modal="true" aria-labelledby="dtoDownloadTitle">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 id="dtoDownloadTitle">DTO 다운로드</h2>
|
||||
<button id="dtoModalCloseButton" class="modal-close" type="button" aria-label="닫기">×</button>
|
||||
<h2 id="dtoDownloadTitle">DTO ?ㅼ슫濡쒕뱶</h2>
|
||||
<button id="dtoModalCloseButton" class="modal-close" type="button" aria-label="?リ린">×</button>
|
||||
</div>
|
||||
<label for="dtoSelect">DTO 선택</label>
|
||||
<label for="dtoSelect">DTO ?좏깮</label>
|
||||
<select id="dtoSelect">
|
||||
<option value="">DTO 목록 불러오는 중...</option>
|
||||
<option value="">DTO 紐⑸줉 遺덈윭?ㅻ뒗 以?..</option>
|
||||
</select>
|
||||
<div class="modal-actions"><button id="dtoConfirmButton" class="primary" type="button">확인</button></div>
|
||||
<div class="modal-actions"><button id="dtoConfirmButton" class="primary" type="button">?뺤씤</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
@@ -95,7 +95,7 @@
|
||||
const select = $('toolSelect'), args = $('arguments'), result = $('result');
|
||||
|
||||
async function loadDtoOptions() {
|
||||
// 서버가 itrf/mci/**/io 패키지에서 찾은 DTO를 _I/_O 접미사 없이 이름 단위로 묶어 셀렉트박스를 구성한다.
|
||||
// ?쒕쾭媛 itrf/mci/**/io ?⑦궎吏?먯꽌 李얠? DTO瑜?_I/_O ?묐????놁씠 ?대쫫 ?⑥쐞濡?臾띠뼱 ??됲듃諛뺤뒪瑜?援ъ꽦?쒕떎.
|
||||
const dtoSelect = $('dtoSelect');
|
||||
try {
|
||||
const response = await fetch('/dto-download/options', { headers: { 'Cache-Control': 'no-cache' } });
|
||||
@@ -108,17 +108,17 @@
|
||||
option.textContent = dtoName;
|
||||
dtoSelect.appendChild(option);
|
||||
}
|
||||
if (!dtoNames.length) dtoSelect.innerHTML = '<option value="">DTO 파일이 없습니다.</option>';
|
||||
if (!dtoNames.length) dtoSelect.innerHTML = '<option value="">DTO ?뚯씪???놁뒿?덈떎.</option>';
|
||||
} catch (error) {
|
||||
dtoSelect.innerHTML = '<option value="">DTO 목록을 불러오지 못했습니다.</option>';
|
||||
dtoSelect.innerHTML = '<option value="">DTO 紐⑸줉??遺덈윭?ㅼ? 紐삵뻽?듬땲??</option>';
|
||||
}
|
||||
}
|
||||
function openDtoModal() { $('dtoDownloadModal').classList.remove('hidden'); loadDtoOptions(); $('dtoSelect').focus(); }
|
||||
function closeDtoModal() { $('dtoDownloadModal').classList.add('hidden'); $('dtoDownloadButton').focus(); }
|
||||
async function downloadSelectedDto() {
|
||||
// 선택한 DTO 이름의 _I/_O 엑셀 생성 API를 순차 호출해 존재하는 파일을 각각 브라우저 다운로드로 저장한다.
|
||||
// ?좏깮??DTO ?대쫫??_I/_O ?묒? ?앹꽦 API瑜??쒖감 ?몄텧??議댁옱?섎뒗 ?뚯씪??媛곴컖 釉뚮씪?곗? ?ㅼ슫濡쒕뱶濡???ν븳??
|
||||
const baseName = $('dtoSelect').value;
|
||||
if (!baseName) return alert('다운로드할 DTO를 선택해주세요.');
|
||||
if (!baseName) return alert('?ㅼ슫濡쒕뱶??DTO瑜??좏깮?댁<?몄슂.');
|
||||
const confirmButton = $('dtoConfirmButton');
|
||||
confirmButton.disabled = true;
|
||||
const errors = [];
|
||||
@@ -131,13 +131,13 @@
|
||||
cache: 'no-store',
|
||||
headers: { 'Cache-Control': 'no-cache' }
|
||||
});
|
||||
if (response.status === 404) continue; // _I/_O 중 한쪽만 존재하는 DTO는 정상 케이스이므로 건너뛴다.
|
||||
if (response.status === 404) continue; // _I/_O 以??쒖そ留?議댁옱?섎뒗 DTO???뺤긽 耳?댁뒪?대?濡?嫄대꼫?대떎.
|
||||
if (!response.ok) {
|
||||
// 422는 GlowTrgmField 메타데이터가 없어 설계서 양식으로 변환할 수 없는 경우다.
|
||||
// 422??GlowTrgmField 硫뷀??곗씠?곌? ?놁뼱 ?ㅺ퀎???묒떇?쇰줈 蹂?섑븷 ???녿뒗 寃쎌슦??
|
||||
const message = (await response.text()).trim();
|
||||
throw new Error(response.status === 422
|
||||
? `${dtoName}: dto의 내용이 엑셀파일 양식에 맞지않습니다 파일을 확인해주세요`
|
||||
: message || `${dtoName} 다운로드에 실패했습니다. (HTTP ${response.status})`);
|
||||
? `${dtoName}: dto???댁슜???묒??뚯씪 ?묒떇??留욎??딆뒿?덈떎 ?뚯씪???뺤씤?댁<?몄슂`
|
||||
: message || `${dtoName} ?ㅼ슫濡쒕뱶???ㅽ뙣?덉뒿?덈떎. (HTTP ${response.status})`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -149,14 +149,14 @@
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
successCount += 1;
|
||||
// 브라우저가 연속 다운로드를 한 번에 차단하지 않도록 다음 요청 전에 짧은 간격을 둔다.
|
||||
// 釉뚮씪?곗?媛 ?곗냽 ?ㅼ슫濡쒕뱶瑜???踰덉뿉 李⑤떒?섏? ?딅룄濡??ㅼ쓬 ?붿껌 ?꾩뿉 吏㏃? 媛꾧꺽???붾떎.
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
} catch (error) {
|
||||
errors.push(error.message);
|
||||
}
|
||||
}
|
||||
if (successCount === 0) {
|
||||
alert(errors.length ? errors.join('\n') : `${baseName} 파일을 찾을 수 없습니다.`);
|
||||
alert(errors.length ? errors.join('\n') : `${baseName} ?뚯씪??李얠쓣 ???놁뒿?덈떎.`);
|
||||
return;
|
||||
}
|
||||
if (errors.length) alert(errors.join('\n'));
|
||||
@@ -179,7 +179,7 @@
|
||||
let response = await fetch('/tool-manifest', { headers: { 'Cache-Control': 'no-cache' } });
|
||||
|
||||
if (!response.ok && response.status === 404) {
|
||||
// Gateway 환경 감지 및 Fallback 처리
|
||||
// Gateway ?섍꼍 媛먯? 諛?Fallback 泥섎━
|
||||
response = await fetch('/mcp/api/v1/tools/list');
|
||||
if (!response.ok) throw new Error(`Gateway HTTP ${response.status}`);
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
state.tools = rpcData.result?.tools || [];
|
||||
isGatewayMode = true;
|
||||
|
||||
$('manifestStatus').textContent = `${state.tools.length} tools · Gateway Mode`;
|
||||
$('manifestStatus').textContent = `${state.tools.length} tools 쨌 Gateway Mode`;
|
||||
$('manifestStatus').className = 'badge ok';
|
||||
renderTools();
|
||||
return;
|
||||
@@ -196,13 +196,13 @@
|
||||
if (!response.ok) throw new Error(`Manifest HTTP ${response.status}`);
|
||||
const manifest = await response.json();
|
||||
state.tools = (manifest.tools || []).filter(tool => tool._meta?.enabled !== false);
|
||||
$('manifestStatus').textContent = `${state.tools.length} tools · rev ${manifest.revision || '-'}`;
|
||||
$('manifestStatus').textContent = `${state.tools.length} tools 쨌 rev ${manifest.revision || '-'}`;
|
||||
$('manifestStatus').className = 'badge ok';
|
||||
renderTools();
|
||||
} catch (error) {
|
||||
$('manifestStatus').textContent = `Load error: ${error.message}`;
|
||||
$('manifestStatus').className = 'badge fail';
|
||||
select.innerHTML = '<option>Tool 목록을 불러오지 못했습니다</option>';
|
||||
select.innerHTML = '<option>Tool 紐⑸줉??遺덈윭?ㅼ? 紐삵뻽?듬땲??/option>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
select.innerHTML = '';
|
||||
for (const tool of tools) {
|
||||
const option = document.createElement('option'); option.value = tool.name;
|
||||
option.textContent = `${tool.name} — ${tool.title || tool.description || ''}`; select.appendChild(option);
|
||||
option.textContent = `${tool.name} ??${tool.title || tool.description || ''}`; select.appendChild(option);
|
||||
}
|
||||
if (tools.length) choose(tools.find(tool => tool.name === state.selected?.name) || tools[0]);
|
||||
else { state.selected = null; $('toolMeta').innerHTML = ''; args.value = ''; }
|
||||
@@ -228,7 +228,7 @@
|
||||
hints.destructiveHint ? '<span class="badge fail">destructive</span>' : ''
|
||||
].join('');
|
||||
args.value = JSON.stringify(exampleForSchema(tool.inputSchema || tool.parametersSchema || {}), null, 2);
|
||||
result.textContent = tool.description || '설명이 없습니다.';
|
||||
result.textContent = tool.description || '?ㅻ챸???놁뒿?덈떎.';
|
||||
resetResult();
|
||||
}
|
||||
|
||||
@@ -252,12 +252,12 @@
|
||||
return 'test-value';
|
||||
}
|
||||
|
||||
function parseArguments() { try { const value = JSON.parse(args.value || '{}'); if (value && typeof value === 'object' && !Array.isArray(value)) return value; throw new Error('요청 JSON은 객체여야 합니다.'); } catch (error) { throw new Error(`요청 JSON 오류: ${error.message}`); } }
|
||||
function parseArguments() { try { const value = JSON.parse(args.value || '{}'); if (value && typeof value === 'object' && !Array.isArray(value)) return value; throw new Error('?붿껌 JSON? 媛앹껜?ъ빞 ?⑸땲??'); } catch (error) { throw new Error(`?붿껌 JSON ?ㅻ쪟: ${error.message}`); } }
|
||||
function endpointFor(tool) {
|
||||
const fallback = `/mcp/${encodeURIComponent(tool.name)}`;
|
||||
try { const endpoint = new URL(tool.endpoint || fallback, window.location.origin); return endpoint.origin === window.location.origin ? `${endpoint.pathname}${endpoint.search}` : fallback; } catch (_) { return fallback; }
|
||||
}
|
||||
function resetResult() { $('httpStatus').textContent = '대기'; $('httpStatus').className = 'badge'; $('latency').textContent = '-'; $('traceId').textContent = 'X-Guid: -'; $('requestId').textContent = 'X-Request-Id: -'; }
|
||||
function resetResult() { $('httpStatus').textContent = '?湲?; $('httpStatus').className = 'badge'; $('latency').textContent = '-'; $('traceId').textContent = 'X-Guid: -'; $('requestId').textContent = 'X-Request-Id: -'; }
|
||||
function toolHeaders(guid, request, session) {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -266,7 +266,7 @@
|
||||
'X-Request-Id': request,
|
||||
'X-Request-Time': new Date().toISOString(),
|
||||
'X-Vrtl-Praf-No': 'V100001',
|
||||
'X-App-Code': 'DASMT',
|
||||
'X-App-Code': 'DATMT',
|
||||
'X-Project-Code': 'AXHUB',
|
||||
'X-User-Ip': '127.0.0.1',
|
||||
'X-Caller-Ip': '127.0.0.1',
|
||||
@@ -278,9 +278,9 @@
|
||||
}
|
||||
|
||||
async function execute(tool = state.selected, body = null) {
|
||||
if (!tool) throw new Error('실행할 Tool을 선택하세요.');
|
||||
if (!tool) throw new Error('?ㅽ뻾??Tool???좏깮?섏꽭??');
|
||||
const payload = body || parseArguments(); const guid = requestId(), request = requestId(), session = requestId(), started = performance.now();
|
||||
$('executeButton').disabled = true; $('httpStatus').textContent = '실행 중'; $('httpStatus').className = 'badge';
|
||||
$('executeButton').disabled = true; $('httpStatus').textContent = '?ㅽ뻾 以?; $('httpStatus').className = 'badge';
|
||||
try {
|
||||
let response;
|
||||
if (isGatewayMode) {
|
||||
@@ -326,31 +326,31 @@
|
||||
|
||||
function saveCase() {
|
||||
try {
|
||||
const payload = parseArguments(); const name = window.prompt('테스트 케이스 이름', state.selected.name);
|
||||
const payload = parseArguments(); const name = window.prompt('?뚯뒪??耳?댁뒪 ?대쫫', state.selected.name);
|
||||
if (!name) return;
|
||||
state.cases.push({ id: requestId(), name, toolName: state.selected.name, arguments: payload, savedAt: new Date().toISOString() }); persistCases();
|
||||
} catch (error) { alert(error.message); }
|
||||
}
|
||||
function renderCases() {
|
||||
const list = $('caseList'); list.innerHTML = '';
|
||||
if (!state.cases.length) { list.innerHTML = '<p style="margin:0;color:#a1a1aa;font-size:13px">저장된 케이스가 없습니다.</p>'; return; }
|
||||
if (!state.cases.length) { list.innerHTML = '<p style="margin:0;color:#a1a1aa;font-size:13px">??λ맂 耳?댁뒪媛 ?놁뒿?덈떎.</p>'; return; }
|
||||
for (const item of state.cases) {
|
||||
const row = document.createElement('div'); row.className = 'case-row';
|
||||
row.innerHTML = `<main><strong>${escapeHtml(item.name)}</strong><small>${escapeHtml(item.toolName)}</small></main><button data-run="${item.id}">실행</button><button class="danger" data-delete="${item.id}">삭제</button>`;
|
||||
row.innerHTML = `<main><strong>${escapeHtml(item.name)}</strong><small>${escapeHtml(item.toolName)}</small></main><button data-run="${item.id}">?ㅽ뻾</button><button class="danger" data-delete="${item.id}">??젣</button>`;
|
||||
list.appendChild(row);
|
||||
}
|
||||
}
|
||||
async function runSavedCases() {
|
||||
if (!state.cases.length) return alert('저장된 테스트 케이스가 없습니다.');
|
||||
$('runAllButton').disabled = true; $('runLog').textContent = `총 ${state.cases.length}건 실행 시작\n`;
|
||||
if (!state.cases.length) return alert('??λ맂 ?뚯뒪??耳?댁뒪媛 ?놁뒿?덈떎.');
|
||||
$('runAllButton').disabled = true; $('runLog').textContent = `珥?${state.cases.length}嫄??ㅽ뻾 ?쒖옉\n`;
|
||||
let success = 0;
|
||||
for (const item of state.cases) {
|
||||
const tool = state.tools.find(candidate => candidate.name === item.toolName);
|
||||
if (!tool) { $('runLog').textContent += `FAIL ${item.name} · Tool 없음\n`; continue; }
|
||||
try { const execution = await execute(tool, item.arguments); success += execution.ok ? 1 : 0; $('runLog').textContent += `${execution.ok ? 'PASS' : 'FAIL'} ${item.name} · HTTP ${execution.status} · ${execution.elapsed}ms\n`; }
|
||||
catch (error) { $('runLog').textContent += `FAIL ${item.name} · ${error.message}\n`; }
|
||||
if (!tool) { $('runLog').textContent += `FAIL ${item.name} 쨌 Tool ?놁쓬\n`; continue; }
|
||||
try { const execution = await execute(tool, item.arguments); success += execution.ok ? 1 : 0; $('runLog').textContent += `${execution.ok ? 'PASS' : 'FAIL'} ${item.name} 쨌 HTTP ${execution.status} 쨌 ${execution.elapsed}ms\n`; }
|
||||
catch (error) { $('runLog').textContent += `FAIL ${item.name} 쨌 ${error.message}\n`; }
|
||||
}
|
||||
$('runLog').textContent += `완료: ${success}/${state.cases.length} 성공`; $('runAllButton').disabled = false;
|
||||
$('runLog').textContent += `?꾨즺: ${success}/${state.cases.length} ?깃났`; $('runAllButton').disabled = false;
|
||||
}
|
||||
|
||||
select.addEventListener('change', () => choose(state.tools.find(tool => tool.name === select.value)));
|
||||
@@ -358,9 +358,9 @@
|
||||
$('sampleButton').addEventListener('click', () => { if (state.selected) args.value = JSON.stringify(exampleForSchema(state.selected.inputSchema || state.selected.parametersSchema || {}), null, 2); });
|
||||
$('reloadButton').addEventListener('click', loadManifest);
|
||||
$('saveButton').addEventListener('click', saveCase);
|
||||
$('executeButton').addEventListener('click', async () => { try { await execute(); } catch (error) { $('httpStatus').textContent = '입력 오류'; $('httpStatus').className = 'badge fail'; result.textContent = error.message; } });
|
||||
$('executeButton').addEventListener('click', async () => { try { await execute(); } catch (error) { $('httpStatus').textContent = '?낅젰 ?ㅻ쪟'; $('httpStatus').className = 'badge fail'; result.textContent = error.message; } });
|
||||
$('runAllButton').addEventListener('click', runSavedCases);
|
||||
$('clearCasesButton').addEventListener('click', () => { if (confirm('저장된 테스트 케이스를 모두 삭제할까요?')) { state.cases = []; persistCases(); } });
|
||||
$('clearCasesButton').addEventListener('click', () => { if (confirm('??λ맂 ?뚯뒪??耳?댁뒪瑜?紐⑤몢 ??젣?좉퉴??')) { state.cases = []; persistCases(); } });
|
||||
$('dtoDownloadButton').addEventListener('click', openDtoModal);
|
||||
$('dtoModalCloseButton').addEventListener('click', closeDtoModal);
|
||||
$('dtoConfirmButton').addEventListener('click', downloadSelectedDto);
|
||||
@@ -372,3 +372,4 @@
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: ax_hub_mcp_local
|
||||
name: ax_hub_mcp_local
|
||||
|
||||
services:
|
||||
gateway:
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
|
||||
was-sal:
|
||||
build:
|
||||
context: ./dat-was-dasmt
|
||||
context: ./dat-was-datmt
|
||||
dockerfile: dat-was-sal/Dockerfile
|
||||
ports:
|
||||
- "8282:8082"
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
|
||||
was-cus:
|
||||
build:
|
||||
context: ./dat-was-dasmt
|
||||
context: ./dat-was-datmt
|
||||
dockerfile: dat-was-cus/Dockerfile
|
||||
ports:
|
||||
- "8284:8084"
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
|
||||
was-pro:
|
||||
build:
|
||||
context: ./dat-was-dasmt
|
||||
context: ./dat-was-datmt
|
||||
dockerfile: dat-was-pro/Dockerfile
|
||||
ports:
|
||||
- "8285:8085"
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
|
||||
was-sys:
|
||||
build:
|
||||
context: ./dat-was-dasmt
|
||||
context: ./dat-was-datmt
|
||||
dockerfile: dat-was-sys/Dockerfile
|
||||
ports:
|
||||
- "8286:8086"
|
||||
@@ -81,3 +81,4 @@ services:
|
||||
- GLOW_COMMUNICATION_EAI_HOST=http://mci-mock
|
||||
- GLOW_COMMUNICATION_EAI_PORT=8080
|
||||
- SPRING_PROFILES_ACTIVE=local
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: ax_hub_mcp
|
||||
name: ax_hub_mcp
|
||||
|
||||
services:
|
||||
gateway:
|
||||
@@ -13,7 +13,7 @@ services:
|
||||
|
||||
was-sal:
|
||||
build:
|
||||
context: ./dat-was-dasmt
|
||||
context: ./dat-was-datmt
|
||||
dockerfile: dat-was-sal/Dockerfile
|
||||
ports:
|
||||
- "127.0.0.1:9282:8082"
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
|
||||
was-cus:
|
||||
build:
|
||||
context: ./dat-was-dasmt
|
||||
context: ./dat-was-datmt
|
||||
dockerfile: dat-was-cus/Dockerfile
|
||||
ports:
|
||||
- "127.0.0.1:9284:8084"
|
||||
@@ -51,7 +51,7 @@ services:
|
||||
|
||||
was-pro:
|
||||
build:
|
||||
context: ./dat-was-dasmt
|
||||
context: ./dat-was-datmt
|
||||
dockerfile: dat-was-pro/Dockerfile
|
||||
ports:
|
||||
- "127.0.0.1:9285:8085"
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
|
||||
was-sys:
|
||||
build:
|
||||
context: ./dat-was-dasmt
|
||||
context: ./dat-was-datmt
|
||||
dockerfile: dat-was-sys/Dockerfile
|
||||
ports:
|
||||
- "127.0.0.1:9286:8086"
|
||||
@@ -96,3 +96,4 @@ services:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
restart: always
|
||||
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
# dat-was-dasmt OpenShift 배포
|
||||
# dat-was-datmt OpenShift 諛고룷
|
||||
|
||||
이 매니페스트는 `was-sal`, `was-cus`, `was-pro`, `was-sys`를 Gateway 전용 내부 Service로 배포합니다. OpenShift Route는 만들지 않습니다.
|
||||
??留ㅻ땲?섏뒪?몃뒗 `was-sal`, `was-cus`, `was-pro`, `was-sys`瑜?Gateway ?꾩슜 ?대? Service濡?諛고룷?⑸땲?? OpenShift Route??留뚮뱾吏 ?딆뒿?덈떎.
|
||||
|
||||
## 반영 전 준비
|
||||
## 諛섏쁺 ??以鍮?
|
||||
|
||||
1. `k8s/overlays/dev/kustomization.yaml`의 namespace, 내부 Registry 경로, release tag를 실제 개발망 값으로 바꿉니다.
|
||||
2. `k8s/base/configmap.yaml`의 `CHANGE_ME` 값을 개발망 MCI/EXTMCI/EAI 주소로 바꿉니다.
|
||||
3. Secret 값은 Git에 저장하지 말고, 배포 namespace에서 별도로 만듭니다.
|
||||
1. `k8s/overlays/dev/kustomization.yaml`??namespace, ?대? Registry 寃쎈줈, release tag瑜??ㅼ젣 媛쒕컻留?媛믪쑝濡?諛붽퓠?덈떎.
|
||||
2. `k8s/base/configmap.yaml`??`CHANGE_ME` 媛믪쓣 媛쒕컻留?MCI/EXTMCI/EAI 二쇱냼濡?諛붽퓠?덈떎.
|
||||
3. Secret 媛믪? Git????ν븯吏 留먭퀬, 諛고룷 namespace?먯꽌 蹂꾨룄濡?留뚮벊?덈떎.
|
||||
|
||||
```powershell
|
||||
oc -n axhub-dasmt-dev create secret generic dasmt-runtime-secrets --from-env-file=runtime-secrets.env
|
||||
oc -n axhub-datmt-dev create secret generic datmt-runtime-secrets --from-env-file=runtime-secrets.env
|
||||
```
|
||||
|
||||
`runtime-secrets.env`에는 DB 계정·비밀번호, API 키, 인증서 비밀번호 등 민감값만 넣고 저장소에 추가하지 않습니다. 애플리케이션이 필요한 Secret 키를 사용하지 않는 경우에도 빈 Secret은 먼저 생성해야 Pod의 `envFrom.secretRef`가 정상 해석됩니다.
|
||||
`runtime-secrets.env`?먮뒗 DB 怨꾩젙쨌鍮꾨?踰덊샇, API ?? ?몄쬆??鍮꾨?踰덊샇 ??誘쇨컧媛믩쭔 ?j퀬 ??μ냼??異붽??섏? ?딆뒿?덈떎. ?좏뵆由ъ??댁뀡???꾩슂??Secret ?ㅻ? ?ъ슜?섏? ?딅뒗 寃쎌슦?먮룄 鍮?Secret? 癒쇱? ?앹꽦?댁빞 Pod??`envFrom.secretRef`媛 ?뺤긽 ?댁꽍?⑸땲??
|
||||
|
||||
## 렌더링과 적용
|
||||
## ?뚮뜑留곴낵 ?곸슜
|
||||
|
||||
```powershell
|
||||
oc kustomize k8s/overlays/dev
|
||||
oc kustomize k8s/overlays/dev | oc apply --dry-run=client -f -
|
||||
oc apply -k k8s/overlays/dev
|
||||
oc get deployment,pod,svc -n axhub-dasmt-dev
|
||||
oc get deployment,pod,svc -n axhub-datmt-dev
|
||||
```
|
||||
|
||||
## Gateway 연결 확인
|
||||
## Gateway ?곌껐 ?뺤씤
|
||||
|
||||
Gateway가 같은 namespace에 있다면 `http://was-sal:8082`, `http://was-cus:8084`, `http://was-pro:8085`, `http://was-sys:8086`로 호출합니다. 다른 namespace에 있으면 `was-<module>.axhub-dasmt-dev.svc` FQDN을 사용하고 NetworkPolicy에서 Gateway namespace의 ingress를 허용합니다.
|
||||
Gateway媛 媛숈? namespace???덈떎硫?`http://was-sal:8082`, `http://was-cus:8084`, `http://was-pro:8085`, `http://was-sys:8086`濡??몄텧?⑸땲?? ?ㅻⅨ namespace???덉쑝硫?`was-<module>.axhub-datmt-dev.svc` FQDN???ъ슜?섍퀬 NetworkPolicy?먯꽌 Gateway namespace??ingress瑜??덉슜?⑸땲??
|
||||
|
||||
?꾩옱 ?좏뵆由ъ??댁뀡?먮뒗 Actuator health endpoint媛 ?놁쑝誘濡?TCP readiness/liveness probe瑜??ъ슜?⑸땲?? Actuator瑜?異붽????ㅼ뿉??HTTP health probe濡?諛붽씀??寃껋쓣 沅뚯옣?⑸땲??
|
||||
|
||||
현재 애플리케이션에는 Actuator health endpoint가 없으므로 TCP readiness/liveness probe를 사용합니다. Actuator를 추가한 뒤에는 HTTP health probe로 바꾸는 것을 권장합니다.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
apiVersion: v1
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: dasmt-runtime-config
|
||||
name: datmt-runtime-config
|
||||
labels:
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
data:
|
||||
SPRING_PROFILES_ACTIVE: dev
|
||||
TZ: Asia/Seoul
|
||||
@@ -14,3 +14,4 @@ data:
|
||||
GLOW_COMMUNICATION_EXTMCI_PORT: "8080"
|
||||
GLOW_COMMUNICATION_EAI_HOST: CHANGE_ME
|
||||
GLOW_COMMUNICATION_EAI_PORT: "9999"
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
apiVersion: apps/v1
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: was-sal
|
||||
labels:
|
||||
app.kubernetes.io/name: was-sal
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
@@ -14,7 +14,7 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: was-sal
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
containers:
|
||||
- name: was-sal
|
||||
@@ -25,9 +25,9 @@ spec:
|
||||
containerPort: 8082
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: dasmt-runtime-config
|
||||
name: datmt-runtime-config
|
||||
- secretRef:
|
||||
name: dasmt-runtime-secrets
|
||||
name: datmt-runtime-secrets
|
||||
env:
|
||||
- name: AXHUB_TOOL_URL
|
||||
value: http://was-sal:8082
|
||||
@@ -65,7 +65,7 @@ metadata:
|
||||
name: was-cus
|
||||
labels:
|
||||
app.kubernetes.io/name: was-cus
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
@@ -75,7 +75,7 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: was-cus
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
containers:
|
||||
- name: was-cus
|
||||
@@ -86,9 +86,9 @@ spec:
|
||||
containerPort: 8084
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: dasmt-runtime-config
|
||||
name: datmt-runtime-config
|
||||
- secretRef:
|
||||
name: dasmt-runtime-secrets
|
||||
name: datmt-runtime-secrets
|
||||
env:
|
||||
- name: AXHUB_TOOL_URL
|
||||
value: http://was-cus:8084
|
||||
@@ -126,7 +126,7 @@ metadata:
|
||||
name: was-pro
|
||||
labels:
|
||||
app.kubernetes.io/name: was-pro
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
@@ -136,7 +136,7 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: was-pro
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
containers:
|
||||
- name: was-pro
|
||||
@@ -147,9 +147,9 @@ spec:
|
||||
containerPort: 8085
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: dasmt-runtime-config
|
||||
name: datmt-runtime-config
|
||||
- secretRef:
|
||||
name: dasmt-runtime-secrets
|
||||
name: datmt-runtime-secrets
|
||||
env:
|
||||
- name: AXHUB_TOOL_URL
|
||||
value: http://was-pro:8085
|
||||
@@ -187,7 +187,7 @@ metadata:
|
||||
name: was-sys
|
||||
labels:
|
||||
app.kubernetes.io/name: was-sys
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
@@ -197,7 +197,7 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: was-sys
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
containers:
|
||||
- name: was-sys
|
||||
@@ -208,9 +208,9 @@ spec:
|
||||
containerPort: 8086
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: dasmt-runtime-config
|
||||
name: datmt-runtime-config
|
||||
- secretRef:
|
||||
name: dasmt-runtime-secrets
|
||||
name: datmt-runtime-secrets
|
||||
env:
|
||||
- name: AXHUB_TOOL_URL
|
||||
value: http://was-sys:8086
|
||||
@@ -241,3 +241,4 @@ spec:
|
||||
drop: ["ALL"]
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
apiVersion: v1
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: was-sal
|
||||
labels:
|
||||
app.kubernetes.io/name: was-sal
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
@@ -20,7 +20,7 @@ metadata:
|
||||
name: was-cus
|
||||
labels:
|
||||
app.kubernetes.io/name: was-cus
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
@@ -36,7 +36,7 @@ metadata:
|
||||
name: was-pro
|
||||
labels:
|
||||
app.kubernetes.io/name: was-pro
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
@@ -52,7 +52,7 @@ metadata:
|
||||
name: was-sys
|
||||
labels:
|
||||
app.kubernetes.io/name: was-sys
|
||||
app.kubernetes.io/part-of: axhub-dasmt
|
||||
app.kubernetes.io/part-of: axhub-datmt
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
@@ -61,3 +61,4 @@ spec:
|
||||
- name: http
|
||||
port: 8086
|
||||
targetPort: http
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: axhub-dasmt-dev
|
||||
namespace: axhub-datmt-dev
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
@@ -13,14 +13,15 @@ labels:
|
||||
|
||||
images:
|
||||
- name: was-sal
|
||||
newName: image-registry.openshift-image-registry.svc:5000/axhub-dasmt-dev/was-sal
|
||||
newName: image-registry.openshift-image-registry.svc:5000/axhub-datmt-dev/was-sal
|
||||
newTag: latest
|
||||
- name: was-cus
|
||||
newName: image-registry.openshift-image-registry.svc:5000/axhub-dasmt-dev/was-cus
|
||||
newName: image-registry.openshift-image-registry.svc:5000/axhub-datmt-dev/was-cus
|
||||
newTag: latest
|
||||
- name: was-pro
|
||||
newName: image-registry.openshift-image-registry.svc:5000/axhub-dasmt-dev/was-pro
|
||||
newName: image-registry.openshift-image-registry.svc:5000/axhub-datmt-dev/was-pro
|
||||
newTag: latest
|
||||
- name: was-sys
|
||||
newName: image-registry.openshift-image-registry.svc:5000/axhub-dasmt-dev/was-sys
|
||||
newName: image-registry.openshift-image-registry.svc:5000/axhub-datmt-dev/was-sys
|
||||
newTag: latest
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
rootProject.name = 'dat-was-dasmt'
|
||||
rootProject.name = 'dat-was-datmt'
|
||||
|
||||
include 'dat-was-lib'
|
||||
include 'dat-was-cus'
|
||||
|
||||
Reference in New Issue
Block a user