38 Commits

Author SHA1 Message Date
juheelee
a667431de1 feat: 메타 테이블 조회 2026-07-29 17:27:27 +09:00
juheelee
b448e68f45 Merge branch 'main' of https://git.devjun.net/kimhyungsik/ax_hub_mcp_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m49s
2026-07-29 16:50:56 +09:00
juheelee
b68fbb9726 feat: 메타 통합 코드 조회 2026-07-29 16:48:28 +09:00
c6a2f82455 push TEST
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m16s
2026-07-29 16:08:45 +09:00
juheelee
2f5dad05df feat: 메타 통합 코드 조회 2026-07-29 15:34:01 +09:00
jade
b0bf4a9c68 fix: use rsync to clean stale files during host volume sync
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-07-29 13:25:10 +09:00
jade
752aae4dde fix: run gradle natively in runner to avoid isolated mount issue
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m50s
2026-07-29 13:22:03 +09:00
jade
5cb37cfda6 fix: mount correct app path for gradle build
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 4s
2026-07-29 13:16:31 +09:00
jade
2a4b3e68e9 fix: allow gradle build libs to be copied into docker container
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 2m3s
2026-07-29 13:03:11 +09:00
jade
ca1082f1c2 fix: rollback register to false as per user request
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 31s
2026-07-29 12:58:22 +09:00
jade
7b1d3131e6 fix: set register true for tools to fix catalog display
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 33s
2026-07-29 12:55:06 +09:00
jade
139ddbc6ca fix: optimize dockerfile to use external gradle build
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 37s
2026-07-29 12:54:02 +09:00
jade
870ebb723f fix: hide markdown emphasis markers in chat responses
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m22s
2026-07-29 11:35:22 +09:00
jade
a08a7d25df refactor: move team member tool to smp category
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-29 11:30:52 +09:00
jade
60acc5df81 refactor: rename solgit tool to sol
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m23s
2026-07-29 11:23:49 +09:00
jade
f5be5b2514 refactor: remove unused team member legacy types
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m32s
2026-07-29 11:14:44 +09:00
jade
aae4898d18 feat: invoke Solgit MCI before returning dummy response
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m26s
2026-07-29 11:02:10 +09:00
jade
06b724459e feat: add solgitReqList and get_mcp_tool_team_members tools
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m31s
2026-07-29 10:26:32 +09:00
jade
4133b63595 불필요 한 파일 삭제
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m19s
2026-07-28 14:39:50 +09:00
jade
1e18d2bc47 feat: support nested MCP input schema validation
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-28 14:39:31 +09:00
jade
0c89a09ccf docs: Update README.md with Web UI Scaffolder enhancements
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 8m5s
2026-07-27 15:41:32 +09:00
jade
3ce9201cd5 fix: Add throws Exception to MciClient template 2026-07-27 15:37:27 +09:00
jade
be81c095c9 fix: Correct text block alignment and formatting arguments 2026-07-27 15:36:40 +09:00
jade
d4554ce34f fix: Remove excessive indentation in string formatting for UseCaseImpl text block 2026-07-27 15:30:58 +09:00
jade
b6d4ab1bec fix: Remove duplicated package path in MCI DTOs and Converter 2026-07-27 15:26:38 +09:00
jade
2c8497ebee fix: Correct ToolScaffolder template issues 2026-07-27 15:10:15 +09:00
jade
8a93d5d6dc feat: Support 4-letter MCI system code in Scaffolder and refactor Onnba classes 2026-07-27 15:03:13 +09:00
jade
c0de1f4edb fix: Include local fallback tools in generateToolsMarkdown API
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m15s
2026-07-27 14:11:25 +09:00
jade
1ee9f9e802 style: Add chat.html link to all top navigation bars 2026-07-27 14:07:33 +09:00
jade
25f0d2bab5 feat: move Onnba MCI request conversion to use case
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m1s
2026-07-27 14:01:04 +09:00
jade
9b9159a605 feat: Add UI/UX improvements to catalog.html including search, config json copy, and guide modal
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 34s
2026-07-27 14:00:27 +09:00
jade
c8dd43221e ci: Fix Node.js installation by dropping sudo/curl and supporting alpine
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m41s
2026-07-27 13:09:01 +09:00
jade
7b8c38489e ci: Add Node.js installation step to Gitea Actions deploy workflow
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-27 13:07:29 +09:00
jade
63187429b4 feat: validate MCP tool input constraints
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-27 13:01:40 +09:00
jade
d4953d60ec test: fix gateway test package references
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 18s
2026-07-27 12:51:28 +09:00
jade
72891dc2db 불필요한 소스 삭제
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 12:38:31 +09:00
jade
82c27bd468 test: prevent duplicate MCP tool names
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 12:37:35 +09:00
jade
ff5ca4fc7a Fix: Add missing formatted arguments for MCI and EAI ToolScaffolder templates 2026-07-27 11:43:37 +09:00
64 changed files with 1811 additions and 341 deletions

View File

@@ -1,8 +1,7 @@
.git
.git
.gradle
.idea
build/
*/build/
/build/
target/
*/target/
bin/

View File

@@ -12,26 +12,36 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Prepare Environment (Install Node.js & Git & Java 21)
run: |
if command -v apt-get &> /dev/null; then
apt-get update
apt-get install -y nodejs git openjdk-21-jdk rsync
elif command -v apk &> /dev/null; then
apk add --no-cache nodejs git openjdk21 rsync
fi
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Sync Code to Host Volume
run: |
echo "Copying latest code to /app (Host Volume)..."
cp -a . /app/
echo "Copying latest code to /app (Host Volume) and removing stale files..."
if command -v rsync &> /dev/null; then
rsync -a --delete --exclude='.git' . /app/
else
rm -rf /app/dap-* /app/src /app/build.gradle /app/settings.gradle
cp -a . /app/
fi
- name: Deploy Task on Host
run: |
echo "Starting Standard CI/CD Deploy pipeline..."
# 1. gradle 공식 이미지로 바로 컴파일 빌드!
docker run --rm \
-v /home/ubuntu/app/ax_hub_mcp_tool:/app \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /home/ubuntu/.gradle:/home/gradle/.gradle \
-w /app \
gradle:8-jdk21 \
./gradlew bootJar -x test
# 1. runner에 설치된 java로 직접 빌드! (격리된 환경 문제 해결)
cd /app
chmod +x gradlew
./gradlew bootJar -x test
# 2. 빌드 결과 plain jar 아카이브 정리
find /app -name "*-plain.jar" -delete

View File

@@ -1,6 +1,6 @@
# DAP Backend
Spring Boot 기반 DAP 관리자 백엔드 API 서버 및 MCP(Model Context Protocol) Gateway / Tool 분산 서버 프로젝트입니다.
Spring Boot 기반 DAP 관리자 백엔드 API 서버 및 MCP(Model Context Protocol) Gateway / Tool 분산 서버 프로젝트 입니다.
---
@@ -114,11 +114,13 @@ MSA 및 외부 시스템(MCI) 연동 환경의 안정성을 위해 완벽한 3-T
새로운 도메인의 기능을 추가할 때 발생하는 반복적인 설정(보일러플레이트, 설정 파일 복사 등)을 1초 만에 자동화하기 위해 **DAP Developer Portal (Web UI)** 및 **CLI 스캐폴더 2종**을 제공합니다.
### 1. DAP Developer Portal (Web UI) - 가장 추천하는 방식!
이제 더 이상 터미널에서 명령어를 칠 필요가 없습니다. Gateway 모듈에 내장된 웹 화면에서 빈칸만 채우면 코드가 마법처럼 찍혀 나옵니다.
이제 더 이상 터미널에서 명령어를 칠 필요가 없습니다. Gateway 모듈에 내장된 웹 화면에서 빈칸만 채우면 신한라이프 패키지 개발 가이드에 맞춘 코드가 마법처럼 찍혀 나옵니다.
1. **접속 방법**: Gateway 서버 기동 후 브라우저에서 `http://localhost:8081/admin/scaffold.html` 접속
2. **Pod (모듈) 생성 탭**: 모듈명(예: hr)과 포트만 입력하면 독립적인 Spring Boot 모듈이 디렉토리부터 빌드 스크립트까지 완벽히 생성됩니다.
3. **Tool (기능) 생성 탭**: 생성된 모듈에 새로운 툴(서비스/DTO) 코드를 자동으로 주입합니다.
3. **Tool (기능) 생성 탭**: 생성된 모듈에 새로운 툴 코드를 자동으로 주입합니다.
- **MCI 연동 기반 툴 생성**: 4자리 시스템 코드(예: `nclg`)를 기반으로 알맞은 패키지에 `MciNclgClient`, `Converter`, `_I`, `_O` 파일이 정확하게 생성됩니다.
- **완벽한 보일러플레이트 자동화**: `UseCaseImpl` 내부에 컴포넌트(`Client`, `Converter`)가 자동으로 의존성 주입되며, Java 15 Text Block을 활용해 들여쓰기(Indentation)까지 완벽히 정렬된 코드를 제공합니다.
### 2. CLI 스캐폴더 (기존 터미널 방식)
웹 화면을 사용할 수 없는 환경이거나 터미널이 익숙한 경우, 아래 명령어를 통해 CLI 마법사를 사용할 수 있습니다.

View File

@@ -1,40 +0,0 @@
import re
# 1. Clean docker-compose.yml
with open('docker-compose.yml', 'r', encoding='utf-8') as f:
content = f.read()
# Remove tool-email and tool-payment blocks
content = re.sub(r'^\s*tool-(email|payment)-(blue|green):.*?^\s*- SPRING_PROFILES_ACTIVE=\$\{ACTIVE_PROFILE:-local\}\n\n?', '', content, flags=re.MULTILINE | re.DOTALL)
# Remove MCP fallback routes
content = re.sub(r'^\s*- MCP_GATEWAY_FALLBACK_ROUTES_(EMAIL|PAYMENT)=.*?\n', '', content, flags=re.MULTILINE)
with open('docker-compose.yml', 'w', encoding='utf-8') as f:
f.write(content)
# 2. Clean deploy.yml
with open('.gitea/workflows/deploy.yml', 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
for line in lines:
if 'tool-email' in line or 'tool-payment' in line:
# For lines that have multiple tools (like docker compose stop), just remove the specific ones
if 'docker compose stop' in line:
line = re.sub(r' tool-(email|payment)-\$[A-Z_]+', '', line)
new_lines.append(line)
else:
new_lines.append(line)
with open('.gitea/workflows/deploy.yml', 'w', encoding='utf-8') as f:
f.writelines(new_lines)
# 3. Clean nginx/conf.d/default.conf
with open('nginx/conf.d/default.conf', 'r', encoding='utf-8') as f:
content = f.read()
content = re.sub(r'upstream tool-(email|payment) \{.*?\n\}\n\n?', '', content, flags=re.MULTILINE | re.DOTALL)
content = re.sub(r'server \{\n\s+listen 828[35];.*?\n\}\n\n?', '', content, flags=re.MULTILINE | re.DOTALL)
with open('nginx/conf.d/default.conf', 'w', encoding='utf-8') as f:
f.write(content)

View File

@@ -1,19 +1,8 @@
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY dap-tool-core dap-tool-core
COPY dap-gateway dap-gateway
RUN chmod +x gradlew
RUN ./gradlew clean :dap-gateway:build -x test
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
COPY --from=builder /app/dap-gateway/build/libs/*-SNAPSHOT.jar app.jar
COPY dap-gateway/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -112,9 +112,7 @@ public class McpRouterController {
}
}
@GetMapping("/tools/list")
public ResponseEntity<JsonRpcResponse> listTools(
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
private List<ToolMetadata> fetchAllActiveTools() {
List<ToolMetadata> activeTools = redisRegistryService.getAllTools()
.stream()
.filter(ToolMetadata::getVisible)
@@ -148,6 +146,14 @@ public class McpRouterController {
log.warn("Failed to fetch local tools from fallback URL: {}", url);
}
}
return activeTools;
}
@GetMapping("/tools/list")
public ResponseEntity<JsonRpcResponse> listTools(
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
List<ToolMetadata> activeTools = fetchAllActiveTools();
if (categoryKey != null && !categoryKey.trim().isEmpty()) {
activeTools = activeTools.stream()
@@ -161,12 +167,10 @@ public class McpRouterController {
return ResponseEntity.ok(response);
}
@GetMapping(value = "/tools/docs/markdown", produces = "text/markdown;charset=UTF-8")
public ResponseEntity<String> generateToolsMarkdown() {
List<ToolMetadata> tools = redisRegistryService.getAllTools()
.stream()
.filter(ToolMetadata::getVisible)
.collect(Collectors.toList());
List<ToolMetadata> tools = fetchAllActiveTools();
StringBuilder md = new StringBuilder();
md.append("# \uD83E\uDD16 Shinhan AI Tool Catalog\n\n");

View File

@@ -63,8 +63,9 @@ public class ScaffoldingController {
String date = req.get("date");
if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
String clientSystemCode = req.get("clientSystemCode");
return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register);
return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register, clientSystemCode);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}

View File

@@ -380,6 +380,7 @@
<a href="/admin/scaffold.html" style="color:#ffffff;" class="font-semibold">Scaffold</a>
<a href="/catalog.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Catalog</a>
<a href="/playground.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Playground</a>
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
</nav>
</div>
<div class="flex items-center">
@@ -499,6 +500,14 @@
<div class="input-hint">Controls dynamic routing exposure.</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">Target System Code (MCI Only)</label>
<input type="text" class="form-control" name="clientSystemCode" pattern="^[a-zA-Z]{4}$" maxlength="4" minlength="4" placeholder="e.g. cfpa" oninput="this.value = this.value.toLowerCase()">
<div class="input-hint">Required for MCI Protocol (4 letters).</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">

View File

@@ -69,6 +69,7 @@
<a href="/admin/scaffold.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Scaffold</a>
<a href="/catalog.html" style="color:#ffffff;" class="font-semibold">Catalog</a>
<a href="/playground.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Playground</a>
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
</nav>
</div>
<div class="flex items-center">
@@ -79,12 +80,24 @@
<main class="max-w-6xl mx-auto px-6 py-10">
<div class="flex flex-col md:flex-row justify-between items-start md:items-end mb-8">
<div class="max-w-2xl">
<h1 class="text-3xl font-bold tracking-tight mb-2" style="color:#f4f4f5;">Registry Catalog</h1>
<p class="text-sm" style="color:#a1a1aa;">Currently integrated AI function tools across all modules. View parameters, descriptions, and integration types.</p>
<div class="max-w-2xl w-full">
<div class="flex items-center space-x-3 mb-2">
<h1 class="text-3xl font-bold tracking-tight" style="color:#f4f4f5;">Registry Catalog</h1>
<button onclick="openGuideModal()" class="w-6 h-6 rounded-full flex items-center justify-center text-sm font-bold bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-white transition-colors" title="Connection Guide">?</button>
</div>
<p class="text-sm mb-6" style="color:#a1a1aa;">Currently integrated AI function tools across all modules. View parameters, descriptions, and integration types.</p>
<div class="relative max-w-md w-full">
<input type="text" id="searchInput" placeholder="Search tools by name, description, or category..." class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-4 py-2.5 text-sm text-zinc-200 placeholder-zinc-500 focus:outline-none focus:border-blue-500 transition-colors">
<svg class="w-4 h-4 absolute right-3 top-3 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</div>
</div>
<div class="mt-4 md:mt-0">
<button id="copyMdBtn" class="btn-primary flex items-center">
<div class="mt-4 md:mt-0 flex space-x-3 w-full md:w-auto">
<button id="copyJsonBtn" class="btn-primary flex items-center justify-center flex-1 md:flex-none" style="background: linear-gradient(135deg, #10b981, #059669); box-shadow: 0 0 20px rgba(16, 185, 129, 0.2);">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
<span id="copyJsonText">Copy Client Config</span>
</button>
<button id="copyMdBtn" class="btn-primary flex items-center justify-center flex-1 md:flex-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path></svg>
<span id="copyBtnText">Copy Markdown Docs</span>
</button>
@@ -99,13 +112,52 @@
</div>
</main>
<!-- Guide Modal -->
<div id="guideModal" class="fixed inset-0 z-[100] hidden flex items-center justify-center">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeGuideModal()"></div>
<div class="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl w-full max-w-2xl mx-4 overflow-hidden">
<div class="p-5 border-b border-zinc-800 flex justify-between items-center bg-zinc-900/50">
<h3 class="text-lg font-semibold text-zinc-100 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
AI Agent Connection Guide
</h3>
<button onclick="closeGuideModal()" class="text-zinc-400 hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<div class="p-6 space-y-6 max-h-[70vh] overflow-y-auto text-sm text-zinc-300">
<div>
<h4 class="font-semibold text-blue-400 mb-3 flex items-center"><span class="w-5 h-5 rounded bg-blue-500/20 flex items-center justify-center mr-2 text-xs">1</span> Cursor IDE 연동</h4>
<p class="mb-2">1. Cursor 설정 창 열기 (<kbd class="px-1.5 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-xs text-zinc-300">Cmd/Ctrl + ,</kbd>)</p>
<p class="mb-2">2. 사이드바에서 <strong>Features</strong> &gt; <strong>MCP</strong> 메뉴로 이동</p>
<p>3. <strong>+ Add New MCP Server</strong>를 클릭하고, 툴 카테고리의 [Copy SSE URL] 버튼으로 복사한 주소를 `SSE` 타입으로 붙여넣기 하세요.</p>
</div>
<div class="border-t border-zinc-800 pt-6">
<h4 class="font-semibold text-orange-400 mb-3 flex items-center"><span class="w-5 h-5 rounded bg-orange-500/20 flex items-center justify-center mr-2 text-xs">2</span> Claude Desktop 연동</h4>
<p class="mb-2">1. 아래 설정 파일을 텍스트 에디터로 엽니다:</p>
<div class="bg-black/40 p-3 rounded-lg border border-zinc-800/80 font-mono text-xs mb-3 text-zinc-400">
<div class="mb-1"><strong>Mac:</strong> ~/Library/Application Support/Claude/claude_desktop_config.json</div>
<div><strong>Win:</strong> %APPDATA%\Claude\claude_desktop_config.json</div>
</div>
<p>2. 화면 우측 상단의 <strong>[Copy Client Config]</strong> 버튼을 누른 후, 복사된 JSON 코드를 이 파일 안에 그대로 붙여넣습니다.</p>
</div>
<div class="border-t border-zinc-800 pt-6">
<h4 class="font-semibold text-emerald-400 mb-3 flex items-center"><span class="w-5 h-5 rounded bg-emerald-500/20 flex items-center justify-center mr-2 text-xs">3</span> 커스텀 Python/Node Agent 연동</h4>
<p class="mb-2">1. MCP 공식 SDK를 설치합니다. <code class="text-xs bg-zinc-800 px-1.5 py-0.5 rounded text-zinc-300 border border-zinc-700">npm i @modelcontextprotocol/sdk</code></p>
<p>2. SSE Client Transport를 사용하여 복사한 카테고리별 URL로 연결합니다.</p>
</div>
</div>
</div>
</div>
<script>
let allTools = []; // 글로벌 변수로 툴 데이터 보관
async function fetchTools() {
try {
const response = await fetch('/mcp/api/v1/tools/list');
const data = await response.json();
const tools = data.result?.tools || [];
renderCatalog(tools);
allTools = data.result?.tools || [];
renderCatalog(allTools);
} catch (err) {
document.getElementById('catalogContainer').innerHTML = `
<div style="padding:16px; border:1px solid #7f1d1d; background:rgba(127,29,29,0.15); color:#fca5a5; font-size:14px; border-radius:8px;">
@@ -134,7 +186,12 @@
header.style.cssText = 'margin-bottom:16px; padding-bottom:8px; border-bottom:1px solid #27272a; display:flex; align-items:baseline; justify-content:space-between;';
header.innerHTML = `
<h2 style="font-size:20px; font-weight:600; color:#f4f4f5; text-transform:capitalize;">${group}</h2>
<span class="geist-mono" style="font-size:12px; color:#71717a;">${grouped[group].length} items</span>
<div style="display:flex; align-items:center; gap:8px;">
<span class="geist-mono" style="font-size:12px; color:#71717a;">${grouped[group].length} items</span>
<button onclick="copySseUrl('${group.toLowerCase()}', this)" class="hover:text-blue-400 text-zinc-500 transition-colors p-1.5 rounded hover:bg-zinc-800/50" title="Copy SSE URL">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
</button>
</div>
`;
section.appendChild(header);
const grid = document.createElement('div');
@@ -227,6 +284,60 @@
}
});
document.getElementById('searchInput').addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const filtered = allTools.filter(t =>
(t.name && t.name.toLowerCase().includes(query)) ||
(t.displayName && t.displayName.toLowerCase().includes(query)) ||
(t.description && t.description.toLowerCase().includes(query)) ||
(t.categoryKey && t.categoryKey.toLowerCase().includes(query))
);
renderCatalog(filtered);
});
function copySseUrl(category, btnElement) {
const url = `${window.location.origin}/mcp/sse/${category}`;
navigator.clipboard.writeText(url).then(() => {
const originalSvg = btnElement.innerHTML;
btnElement.innerHTML = `<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>`;
setTimeout(() => { btnElement.innerHTML = originalSvg; }, 2000);
}).catch(() => alert('Failed to copy.'));
}
function openGuideModal() {
document.getElementById('guideModal').classList.remove('hidden');
}
function closeGuideModal() {
document.getElementById('guideModal').classList.add('hidden');
}
document.getElementById('copyJsonBtn').addEventListener('click', () => {
const groups = [...new Set(allTools.map(t => t.categoryKey || 'Others'))];
const config = { mcpServers: {} };
groups.forEach(g => {
const safeKey = g.toLowerCase().replace(/[^a-z0-9]/g, '-');
config.mcpServers[`axhub-${safeKey}`] = {
"command": "node",
"args": [],
"env": {},
"type": "sse",
"url": `${window.location.origin}/mcp/sse/${g.toLowerCase()}`
};
// Note: Claude desktop expects command/args for stdio, but for SSE it usually accepts just url/type or requires specialized settings.
// Adjusted to basic config.
delete config.mcpServers[`axhub-${safeKey}`].command;
delete config.mcpServers[`axhub-${safeKey}`].args;
delete config.mcpServers[`axhub-${safeKey}`].env;
});
const textSpan = document.getElementById('copyJsonText');
const originalText = textSpan.textContent;
navigator.clipboard.writeText(JSON.stringify(config, null, 2)).then(() => {
textSpan.textContent = 'Copied JSON!';
setTimeout(() => { textSpan.textContent = originalText; }, 2000);
}).catch(() => alert('Failed to copy JSON.'));
});
document.addEventListener('DOMContentLoaded', fetchTools);
</script>
</body>

View File

@@ -173,6 +173,10 @@
.replace(/'/g, "&#039;");
}
function removeMarkdownEmphasis(text) {
return text.replace(/\*\*/g, '');
}
async function sendMessage() {
const text = chatInput.value.trim();
if (!text || isLoading) return;
@@ -205,6 +209,7 @@
// 5. 스트림 읽기
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let responseText = '';
while (true) {
const { done, value } = await reader.read();
@@ -215,7 +220,8 @@
for (const line of lines) {
if (line.startsWith('data:')) {
const data = line.substring(5);
textContainer.innerHTML += escapeHtml(data);
responseText += data;
textContainer.textContent = removeMarkdownEmphasis(responseText);
scrollToBottom();
}
}

View File

@@ -111,6 +111,7 @@
<a href="/admin/scaffold.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Scaffold</a>
<a href="/catalog.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Catalog</a>
<a href="/playground.html" style="color:#ffffff;" class="font-semibold">Playground</a>
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
</nav>
</div>
<div class="flex items-center space-x-3">

View File

@@ -7,6 +7,7 @@ import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import io.shinhanlife.dap.mcg.sync.DynamicMcpController;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

View File

@@ -5,7 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.sync.RegistryMcpToolSpecificationFactory;
import org.junit.jupiter.api.Test;
class RegistryMcpToolSpecificationFactoryTest {

View File

@@ -0,0 +1,30 @@
package io.shinhanlife.dap.lib.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @package io.shinhanlife.dap.lib.annotation
* @className McpValidation
* @description Declares JSON Schema validation constraints for MCP tool input fields
* @author 0986406
* @create 2026.07.27
* <pre>
* ---------- revision history ----------
* date author description
* ---------- --------- ---------------------------
* 2026.07.27 0986406 initial creation
* </pre>
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpValidation {
boolean required() default false;
String pattern() default "";
long minimum() default Long.MIN_VALUE;
String[] allowedValues() default {};
}

View File

@@ -3,11 +3,16 @@ package io.shinhanlife.dap.lib.util;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@@ -30,17 +35,23 @@ public class JsonSchemaGenerator {
* Java DTO 클래스를 분석하여 MCP 규격의 완전한 JSON Schema를 생성합니다.
*/
public static Map<String, Object> generateSchema(Class<?> clazz) {
return generateSchema(clazz, new HashSet<>());
}
private static Map<String, Object> generateSchema(Class<?> clazz, Set<Class<?>> visiting) {
Map<String, Object> schema = new HashMap<>();
schema.put("type", "object");
if (!visiting.add(clazz)) {
return schema;
}
Map<String, Object> properties = new HashMap<>();
List<String> requiredList = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
Map<String, Object> fieldSchema = new HashMap<>();
Map<String, Object> fieldSchema = createFieldSchema(field, visiting);
// 1. 타입 매핑
fieldSchema.put("type", mapJavaTypeToJsonType(field.getType()));
// 2. 어노테이션 기반 설명 추출
McpParameter paramAnnotation = field.getAnnotation(McpParameter.class);
@@ -58,6 +69,20 @@ public class JsonSchemaGenerator {
if ((jsonProp != null && jsonProp.required()) || (paramAnnotation != null && paramAnnotation.required())) {
requiredList.add(field.getName());
}
McpValidation validation = field.getAnnotation(McpValidation.class);
if (validation != null && validation.required() && !requiredList.contains(field.getName())) {
requiredList.add(field.getName());
}
if (validation != null && !validation.pattern().isEmpty()) {
fieldSchema.put("pattern", validation.pattern());
}
if (validation != null && validation.minimum() != Long.MIN_VALUE) {
fieldSchema.put("minimum", validation.minimum());
}
if (validation != null && validation.allowedValues().length > 0) {
fieldSchema.put("enum", List.of(validation.allowedValues()));
}
properties.put(field.getName(), fieldSchema);
}
@@ -67,9 +92,47 @@ public class JsonSchemaGenerator {
schema.put("required", requiredList);
}
visiting.remove(clazz);
return schema;
}
private static Map<String, Object> createFieldSchema(Field field, Set<Class<?>> visiting) {
Class<?> fieldType = field.getType();
if (isSimpleType(fieldType)) {
return new HashMap<>(Map.of("type", mapJavaTypeToJsonType(fieldType)));
}
if (List.class.isAssignableFrom(fieldType)) {
Map<String, Object> fieldSchema = new HashMap<>();
fieldSchema.put("type", "array");
fieldSchema.put("items", generateItemsSchema(field, visiting));
return fieldSchema;
}
return generateSchema(fieldType, visiting);
}
private static Map<String, Object> generateItemsSchema(Field field, Set<Class<?>> visiting) {
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType parameterizedType) {
Type itemType = parameterizedType.getActualTypeArguments()[0];
if (itemType instanceof Class<?> itemClass) {
if (isSimpleType(itemClass)) {
return new HashMap<>(Map.of("type", mapJavaTypeToJsonType(itemClass)));
}
return generateSchema(itemClass, visiting);
}
}
return new HashMap<>(Map.of("type", "object"));
}
private static boolean isSimpleType(Class<?> clazz) {
return clazz == String.class
|| clazz == Integer.class || clazz == int.class
|| clazz == Long.class || clazz == long.class
|| clazz == Double.class || clazz == double.class
|| clazz == Float.class || clazz == float.class
|| clazz == Boolean.class || clazz == boolean.class;
}
private static String mapJavaTypeToJsonType(Class<?> clazz) {
if (clazz == String.class) return "string";
if (clazz == Integer.class || clazz == int.class) return "integer";

View File

@@ -68,7 +68,7 @@ public class ToolScaffolder {
String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
if (createDate.trim().isEmpty()) createDate = defaultDate;
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true);
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null);
System.out.println(result);
}
@@ -80,7 +80,7 @@ public class ToolScaffolder {
return scanner.nextLine().trim();
}
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register) throws IOException {
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode) throws IOException {
baseName = toPascalCase(baseName);
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
@@ -95,6 +95,18 @@ public class ToolScaffolder {
String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase();
boolean isMci = "MCI".equalsIgnoreCase(routingType);
String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase();
String clientPkgSuffix = "";
String clientPrefixCap = "";
Path mciClientDir = null;
if (isMci && clientSystemCode != null && clientSystemCode.length() == 4) {
String clientPrefix = clientSystemCode.toLowerCase();
clientPkgSuffix = clientPrefix.substring(0, 3) + "." + clientPrefix.substring(3, 4);
clientPrefixCap = toPascalCase(clientSystemCode);
mciGroupPath = "infra/itrf/mci/" + clientPrefix.substring(0, 3) + "/" + clientPrefix.substring(3, 4);
mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath));
}
Path mciIoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath, "io"));
Files.createDirectories(usecaseDir);
@@ -102,6 +114,9 @@ public class ToolScaffolder {
Files.createDirectories(dtoDir);
if (isMci) {
Files.createDirectories(mciIoDir);
if (mciClientDir != null) {
Files.createDirectories(mciClientDir);
}
} else {
Files.createDirectories(legacyDtoDir);
}
@@ -205,7 +220,7 @@ public class ToolScaffolder {
bizPackage, baseName,
routingType, group.toLowerCase(),
baseName,
baseName, toolName, description, description + " 줘.", interfaceId, register,
baseName, toolName, description, description + " ?줘.", interfaceId, register,
baseName
);
@@ -226,8 +241,9 @@ public class ToolScaffolder {
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import %s.converter.%sLegacyConverter;
import %s.infra.itrf.mci.%s.io.%s_I;
import %s.converter.%sConverter;
import %s.%s.io.%s_I;
%s
/**
* @package %s.usecase.impl
@@ -248,8 +264,8 @@ public class ToolScaffolder {
@RequiredArgsConstructor
public class %sUseCaseImpl implements %sUseCase {
private final AxhubMciComponent mci;
private final %sLegacyConverter converter;
%s
private final %sConverter converter;
@Override
public Object execute(%sRequest req) {
@@ -276,14 +292,20 @@ public class ToolScaffolder {
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
BASE_PACKAGE, group.toLowerCase(), interfaceId,
bizPackage, baseName,
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
(clientPrefixCap.isEmpty() ? "" : "import " + BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".Mci" + clientPrefixCap + "Client;\n"),
bizPackage,
baseName,
author,
createDate,
createDate, author,
baseName, baseName,
baseName,
baseName, toolName,
baseName,
(clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"),
baseName,
baseName,
toolName,
interfaceId,
interfaceId
);
@@ -295,7 +317,7 @@ public class ToolScaffolder {
import %s.dto.%sResponse;
import %s.usecase.%sUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import %s.converter.%sLegacyConverter;
import %s.converter.%sConverter;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -319,7 +341,7 @@ public class ToolScaffolder {
@RequiredArgsConstructor
public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase {
private final %sLegacyConverter converter;
private final %sConverter converter;
@Override
public Object execute(%sRequest req) {
@@ -333,6 +355,7 @@ public class ToolScaffolder {
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
author,
createDate,
createDate, author,
@@ -348,12 +371,12 @@ public class ToolScaffolder {
if (isMci) {
String mciReqContent = """
package %s.infra.itrf.mci.%s.io;
package %s.%s.io;
import lombok.Data;
/**
* @package %s.infra.itrf.mci.%s.io
* @package %s.%s.io
* @className %s_I
* @description AX HUB 시스템 처리 클래스
* @author %s
@@ -378,16 +401,16 @@ public class ToolScaffolder {
*/
private String content;
}
""".formatted(BASE_PACKAGE, group.toLowerCase(), BASE_PACKAGE, group.toLowerCase(), interfaceId, author, createDate, createDate, author, interfaceId);
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
String mciResContent = """
package %s.infra.itrf.mci.%s.io;
package %s.%s.io;
import lombok.Data;
/**
* @package %s.infra.itrf.mci.%s.io
* @package %s.%s.io
* @className %s_O
* @description AX HUB 시스템 처리 클래스
* @author %s
@@ -404,7 +427,7 @@ public class ToolScaffolder {
public class %s_O {
// TODO: Add response fields here
}
""".formatted(BASE_PACKAGE, group.toLowerCase(), BASE_PACKAGE, group.toLowerCase(), interfaceId, author, createDate, createDate, author, interfaceId);
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
String converterContent = """
@@ -412,15 +435,15 @@ public class ToolScaffolder {
import %s.dto.%sRequest;
import %s.dto.%sResponse;
import %s.infra.itrf.mci.%s.io.%s_I;
import %s.infra.itrf.mci.%s.io.%s_O;
import %s.%s.io.%s_I;
import %s.%s.io.%s_O;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @package %s.converter
* @className %sLegacyConverter
* @className %sConverter
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
@@ -433,7 +456,7 @@ public class ToolScaffolder {
* </pre>
*/
@Mapper(componentModel = "spring")
public interface %sLegacyConverter {
public interface %sConverter {
@Mapping(source = "phoneNumber", target = "phone")
@Mapping(source = "message", target = "content")
@@ -449,14 +472,14 @@ public class ToolScaffolder {
bizPackage,
bizPackage, baseName,
bizPackage, baseName,
BASE_PACKAGE, group.toLowerCase(), interfaceId,
BASE_PACKAGE, group.toLowerCase(), interfaceId,
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
bizPackage, baseName, author, createDate, createDate, author,
baseName, interfaceId, baseName,
baseName, interfaceId,
baseName, interfaceId
);
Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent);
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
@@ -467,7 +490,47 @@ public class ToolScaffolder {
log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n");
log.append("[MCI Request IO] ").append(mciIoDir.resolve(interfaceId + "_I.java")).append("\n");
log.append("[MCI Response IO] ").append(mciIoDir.resolve(interfaceId + "_O.java")).append("\n");
log.append("[MCI Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\n");
log.append("[MCI Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
if (!clientPrefixCap.isEmpty()) {
String mciClientContent = """
package %s.%s;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
/**
* @package %s.%s
* @className Mci%sClient
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Component
@RequiredArgsConstructor
public class Mci%sClient {
private final AxhubMciComponent mci;
public Transfer<Object> callTo(String interfaceId, String dummy, Object mciReq, Class<Object> resType) throws Exception {
return mci.callTo(interfaceId, dummy, mciReq, resType);
}
}
""".formatted(
BASE_PACKAGE, mciGroupPath.replace("/", "."),
BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap
);
Files.writeString(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent);
log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n");
}
} else {
String legacyReqContent = """
@@ -543,7 +606,7 @@ public class ToolScaffolder {
/**
* @package %s.converter
* @className %sLegacyConverter
* @className %sConverter
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
@@ -556,7 +619,7 @@ public class ToolScaffolder {
* </pre>
*/
@Mapper(componentModel = "spring")
public interface %sLegacyConverter {
public interface %sConverter {
@Mapping(source = "phoneNumber", target = "phone")
@Mapping(source = "message", target = "content")
@@ -577,7 +640,7 @@ public class ToolScaffolder {
bizPackage, baseName, author, createDate, createDate, author,
baseName, baseName, baseName, baseName, baseName, baseName, baseName
);
Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent);
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
@@ -588,7 +651,7 @@ public class ToolScaffolder {
log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n");
log.append("[Legacy Request DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRequest.java")).append("\n");
log.append("[Legacy Response DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyResponse.java")).append("\n");
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\n");
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
}
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");

View File

@@ -0,0 +1,149 @@
package io.shinhanlife.dap.lib.validation;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @package io.shinhanlife.dap.lib.validation
* @className McpToolNameValidator
* @description Validates unique MCP function names across tool modules
* @author 0986406
* @create 2026.07.27
* <pre>
* ---------- revision history ----------
* date author description
* ---------- --------- ---------------------------
* 2026.07.27 0986406 initial creation
* </pre>
*/
public final class McpToolNameValidator {
private static final Pattern TOOL_NAME_PATTERN = Pattern.compile("\\bname\\s*=\\s*\\\"([^\\\"]+)\\\"");
private McpToolNameValidator() {
}
public static void assertUnique(Path projectRoot) {
Map<String, List<ToolDeclaration>> declarationsByName = new LinkedHashMap<>();
try (var modules = Files.list(projectRoot)) {
modules.filter(Files::isDirectory)
.filter(path -> path.getFileName().toString().startsWith("dap-tool-"))
.filter(path -> !path.getFileName().toString().equals("dap-tool-core"))
.sorted()
.forEach(module -> collectDeclarations(module, declarationsByName));
} catch (IOException exception) {
throw new UncheckedIOException("Failed to scan MCP tool modules", exception);
}
List<Map.Entry<String, List<ToolDeclaration>>> duplicates = declarationsByName.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.sorted(Map.Entry.comparingByKey())
.toList();
if (!duplicates.isEmpty()) {
throw new IllegalStateException(buildDuplicateMessage(duplicates));
}
}
private static void collectDeclarations(Path module, Map<String, List<ToolDeclaration>> declarationsByName) {
Path sourceDirectory = module.resolve("src/main/java");
if (!Files.isDirectory(sourceDirectory)) {
return;
}
try (var sources = Files.walk(sourceDirectory)) {
sources.filter(path -> path.toString().endsWith(".java"))
.sorted()
.forEach(source -> collectDeclarations(module.getFileName().toString(), source, declarationsByName));
} catch (IOException exception) {
throw new UncheckedIOException("Failed to scan module " + module.getFileName(), exception);
}
}
private static void collectDeclarations(String moduleName, Path source,
Map<String, List<ToolDeclaration>> declarationsByName) {
String content;
try {
content = Files.readString(source);
} catch (IOException exception) {
throw new UncheckedIOException("Failed to read " + source, exception);
}
int annotationOffset = content.indexOf("@McpFunction");
while (annotationOffset >= 0) {
int openingParenthesis = content.indexOf('(', annotationOffset);
int closingParenthesis = findAnnotationEnd(content, openingParenthesis);
if (openingParenthesis < 0 || closingParenthesis < 0) {
break;
}
Matcher matcher = TOOL_NAME_PATTERN.matcher(content.substring(openingParenthesis + 1, closingParenthesis));
if (matcher.find()) {
String toolName = matcher.group(1);
int line = 1 + (int) content.substring(0, annotationOffset).chars().filter(character -> character == '\n').count();
declarationsByName.computeIfAbsent(toolName, ignored -> new ArrayList<>())
.add(new ToolDeclaration(moduleName, source, line));
}
annotationOffset = content.indexOf("@McpFunction", closingParenthesis + 1);
}
}
private static int findAnnotationEnd(String content, int openingParenthesis) {
if (openingParenthesis < 0) {
return -1;
}
int depth = 0;
boolean inString = false;
boolean escaped = false;
for (int index = openingParenthesis; index < content.length(); index++) {
char character = content.charAt(index);
if (inString) {
if (escaped) {
escaped = false;
} else if (character == '\\') {
escaped = true;
} else if (character == '\"') {
inString = false;
}
continue;
}
if (character == '\"') {
inString = true;
} else if (character == '(') {
depth++;
} else if (character == ')' && --depth == 0) {
return index;
}
}
return -1;
}
private static String buildDuplicateMessage(List<Map.Entry<String, List<ToolDeclaration>>> duplicates) {
StringBuilder message = new StringBuilder("Duplicate MCP tool name(s):");
for (Map.Entry<String, List<ToolDeclaration>> duplicate : duplicates) {
message.append("\n\n").append(duplicate.getKey());
duplicate.getValue().stream()
.sorted(Comparator.comparing(ToolDeclaration::moduleName).thenComparing(declaration -> declaration.source().toString()))
.forEach(declaration -> message.append("\n- ")
.append(declaration.moduleName())
.append(": ")
.append(declaration.source())
.append(':').append(declaration.line()));
}
return message.toString();
}
private record ToolDeclaration(String moduleName, Path source, int line) {
}
}

View File

@@ -190,11 +190,13 @@ public class BusinessToolController {
}
}
// 4. 메서드 실행
long startTime = System.currentTimeMillis();
Object methodResult = null;
methodResult = targetMethod.invoke(targetBean, invokeArgument);
if (targetMethod.getParameterCount() == 0) {
methodResult = targetMethod.invoke(targetBean);
} else {
methodResult = targetMethod.invoke(targetBean, invokeArgument);
}
long elapsed = System.currentTimeMillis() - startTime;

View File

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

View File

@@ -0,0 +1,121 @@
package io.shinhanlife.dap.lib.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
/**
* @package io.shinhanlife.dap.lib.util
* @className JsonSchemaGeneratorTest
* @description JSON Schema constraint generation test
* @author 0986406
* @create 2026.07.27
* <pre>
* ---------- revision history ----------
* date author description
* ---------- --------- ---------------------------
* 2026.07.27 0986406 initial creation
* </pre>
*/
class JsonSchemaGeneratorTest {
@Test
void includesMcpParameterConstraintsInGeneratedSchema() {
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ValidatedRequest.class);
Map<String, Map<String, Object>> properties = properties(schema);
assertTrue(((List<String>) schema.get("required")).contains("phoneNumber"));
assertTrue(((List<String>) schema.get("required")).contains("approvalStatus"));
assertEquals("^01[0-9]{8,9}$", properties.get("phoneNumber").get("pattern"));
assertEquals(1L, properties.get("amount").get("minimum"));
assertEquals(List.of("APPROVE", "REJECT"), properties.get("approvalStatus").get("enum"));
}
@Test
void includesNestedDtoConstraintsInGeneratedSchema() {
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(NestedRequest.class);
Map<String, Object> childSchema = property(schema, "child");
assertEquals("object", childSchema.get("type"));
assertTrue(required(childSchema).contains("businessDate"));
assertEquals("^\\d{8}$", property(childSchema, "businessDate").get("pattern"));
}
@Test
void includesNestedDtoSchemaForListItems() {
Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ListRequest.class);
Map<String, Object> itemSchema = map(property(schema, "items").get("items"));
assertEquals("object", itemSchema.get("type"));
assertTrue(required(itemSchema).contains("businessDate"));
}
@Test
void validatorRejectsInvalidNestedValue() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
JsonSchema schema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)
.getSchema(objectMapper.writeValueAsString(JsonSchemaGenerator.generateSchema(NestedRequest.class)));
Set<ValidationMessage> errors = schema.validate(objectMapper.valueToTree(Map.of(
"child", Map.of("businessDate", "2026-07-28"))));
assertFalse(errors.isEmpty());
}
@SuppressWarnings("unchecked")
private Map<String, Map<String, Object>> properties(Map<String, Object> schema) {
return (Map<String, Map<String, Object>>) schema.get("properties");
}
private Map<String, Object> property(Map<String, Object> schema, String name) {
return properties(schema).get(name);
}
@SuppressWarnings("unchecked")
private Map<String, Object> map(Object value) {
return (Map<String, Object>) value;
}
@SuppressWarnings("unchecked")
private List<String> required(Map<String, Object> schema) {
return (List<String>) schema.get("required");
}
private static class ValidatedRequest {
@McpParameter(description = "recipient phone number", required = true)
@McpValidation(pattern = "^01[0-9]{8,9}$")
private String phoneNumber;
@McpParameter(description = "issue amount", required = true)
@McpValidation(minimum = 1)
private Long amount;
@McpParameter(description = "approval result")
@McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"})
private String approvalStatus;
}
private static class NestedRequest {
private NestedChild child;
}
private static class ListRequest {
private List<NestedChild> items;
}
private static class NestedChild {
@McpValidation(required = true, pattern = "^\\d{8}$")
private String businessDate;
}
}

View File

@@ -0,0 +1,91 @@
package io.shinhanlife.dap.lib.validation;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
* @package io.shinhanlife.dap.lib.validation
* @className McpToolNameValidatorTest
* @description MCP tool name uniqueness validation test
* @author 0986406
* @create 2026.07.27
* <pre>
* ---------- revision history ----------
* date author description
* ---------- --------- ---------------------------
* 2026.07.27 0986406 initial creation
* </pre>
*/
class McpToolNameValidatorTest {
@TempDir
Path temporaryRoot;
@Test
void rejectsDuplicateMcpFunctionNamesAcrossToolModules() throws IOException {
writeToolSource("dap-tool-first", "FirstTool.java", "first", "send_sms");
writeToolSource("dap-tool-second", "SecondTool.java", "second", "send_sms");
IllegalStateException exception = assertThrows(IllegalStateException.class,
() -> McpToolNameValidator.assertUnique(temporaryRoot));
assertTrue(exception.getMessage().contains("send_sms"));
assertTrue(exception.getMessage().contains("dap-tool-first"));
assertTrue(exception.getMessage().contains("dap-tool-second"));
}
@Test
void acceptsCurrentProjectToolNames() {
assertDoesNotThrow(() -> McpToolNameValidator.assertUnique(findProjectRoot()));
}
@Test
void usesDomainSpecificNamesForCustomerBillingAndBondTools() throws IOException {
Path root = findProjectRoot();
assertToolName(root, "dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java",
"customer_detail", "detail");
assertToolName(root, "dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java",
"billing_process", "process");
assertToolName(root, "dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java",
"bond_issue", "issue");
}
private void assertToolName(Path root, String relativePath, String expectedName, String legacyName) throws IOException {
String source = Files.readString(root.resolve(relativePath));
assertTrue(source.contains("name = \"" + expectedName + "\""));
assertFalse(source.contains("name = \"" + legacyName + "\""));
}
private void writeToolSource(String moduleName, String fileName, String className, String toolName) throws IOException {
Path source = temporaryRoot.resolve(moduleName).resolve("src/main/java/example").resolve(fileName);
Files.createDirectories(source.getParent());
Files.writeString(source, """
package example;
class %s {
@McpFunction(displayName = "%s", name = "%s", description = "test")
void execute() { }
}
""".formatted(className, className, toolName));
}
private Path findProjectRoot() {
Path current = Path.of("").toAbsolutePath();
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
current = current.getParent();
}
if (current == null) {
throw new IllegalStateException("Could not locate Gradle project root");
}
return current;
}
}

View File

@@ -1,55 +0,0 @@
package io.shinhanlife.dap.mcc.sample.converter;
/**
* @package io.shinhanlife.dap.mcc.sample.converter
* @className SampleLegacyConverterTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcc.sample.dto.SampleAiReqDto;
import io.shinhanlife.dap.mcc.sample.dto.SampleLegacyReqDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class SampleLegacyConverterTest {
// MapStruct가 자동 생성한 구현체를 가져와서 테스트합니다. (IDE 에러 방지를 위해 INSTANCE 참조)
private final SampleLegacyConverter sampleLegacyConverter = SampleLegacyConverter.INSTANCE;
@Test
@DisplayName("AI 파라미터 DTO가 MCI DTO로 정확히 매핑되는지 테스트")
public void testAiToMciMapping() {
// given: AI Agent가 준 깔끔한 형태의 파라미터 생성
SampleAiReqDto aiDto = SampleAiReqDto.builder()
.userId("HONG_GILDONG")
.actionType("SOATM0100R")
.extraInfo("휴가신청내역 조회")
.build();
// when: MapStruct 자동 생성 매퍼를 통해 1줄로 변환
SampleLegacyReqDto mciDto = sampleLegacyConverter.toLegacyRequest(aiDto);
// then: 콘솔에 결과 출력 및 값 검증
System.out.println("====== MapStruct 변환 테스트 결과 ======");
System.out.println(" [변환 전] AI DTO : " + aiDto);
System.out.println(" [변환 후] Legacy DTO: " + mciDto);
System.out.println("===============================================");
assertNotNull(mciDto, "변환된 객체는 null이 아니어야 합니다.");
assertEquals("HONG_GILDONG", mciDto.getCustomerId(), "userId -> customerId 매핑 성공");
assertEquals("SOATM0100R", mciDto.getInterfaceId(), "actionType -> interfaceId 매핑 성공");
assertEquals("휴가신청내역 조회", mciDto.getRequestDetails(), "extraInfo -> requestDetails 매핑 성공");
}
}

View File

@@ -1,19 +1,8 @@
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY dap-tool-core dap-tool-core
COPY dap-tool-oth dap-tool-oth
RUN chmod +x gradlew
RUN ./gradlew clean :dap-tool-oth:build -x test
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
COPY --from=builder /app/dap-tool-oth/build/libs/*-SNAPSHOT.jar app.jar
COPY dap-tool-oth/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8084
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -0,0 +1,31 @@
package io.shinhanlife.dap.mcc.biz.cmm.converter;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeResponse;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_O;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.converter
* @className MetaCommonCodeConverter
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public interface MetaCommonCodeConverter {
@Mapping(target = "csNo", source = "groupCode", defaultValue = "GRP_COMM_CD")
CLCNNB00001_I toLegacyRequest(MetaCommonCodeRequest req);
@Mapping(target = "codeList", ignore = true)
MetaCommonCodeResponse toResponse(CLCNNB00001_O mciRes);
}

View File

@@ -0,0 +1,31 @@
package io.shinhanlife.dap.mcc.biz.cmm.converter;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableResponse;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_O;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.converter
* @className MetaTableConverter
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public interface MetaTableConverter {
@Mapping(target = "csNo", source = "tableName", defaultValue = "TB_META_BAS")
CLCNNB00001_I toLegacyRequest(MetaTableRequest req);
@Mapping(target = "tableList", ignore = true)
MetaTableResponse toResponse(CLCNNB00001_O mciRes);
}

View File

@@ -16,10 +16,12 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto;
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import lombok.Data;
@Data
public class BalanceRequest {
@McpParameter(description = "고객의 계좌번호 (- 제외)", required = true)
@McpValidation(pattern = "\\S")
private String accountNumber;
}

View File

@@ -5,6 +5,7 @@ import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import lombok.Getter;
import lombok.Setter;
@@ -30,8 +31,10 @@ import lombok.Setter;
public class BillingProcessRequest {
@McpParameter(description = "처리할 청구 접수 번호", required = true)
@McpValidation(pattern = "\\S")
private String billingId;
@McpParameter(description = "심사 승인 여부 (예: APPROVE, REJECT)")
@McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"})
private String approvalStatus;
}
}

View File

@@ -5,6 +5,7 @@ import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import lombok.Getter;
import lombok.Setter;
@@ -30,8 +31,10 @@ import lombok.Setter;
public class BondIssueRequest {
@McpParameter(description = "발행할 디지털 증권 금액", required = true)
@McpValidation(minimum = 1)
private Long amount;
@McpParameter(description = "발행 대상 계좌 번호", required = true)
@McpValidation(pattern = "\\S")
private String targetAccount;
}
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Data;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
* @className MetaCommonCodeRequest
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MetaCommonCodeRequest {
@McpParameter(description = "통합코드 그룹 ID (예: GRP_SYS_01, GRP_COMM_CD)", required = false)
private String groupCode;
@McpParameter(description = "코드명 검색 키워드 (예: 사용, 상태)", required = false)
private String codeName;
@McpParameter(description = "사용여부 (예: Y, N)", required = false)
private String useYn;
}

View File

@@ -0,0 +1,37 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
* @className MetaCommonCodeResponse
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MetaCommonCodeResponse {
private List<MetaCommonCodeItem> codeList;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class MetaCommonCodeItem {
private String groupCode;
private String code;
private String codeName;
private String codeDesc;
private Integer sortSeq;
private String useYn;
}
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Data;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
* @className MetaTableRequest
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MetaTableRequest {
@McpParameter(description = "테이블 물리명 키워드 (예: TB_CUST_BAS, TB_CONT)", required = false)
private String tableName;
@McpParameter(description = "테이블 논리명(한글) 키워드 (예: 고객기본, 계약)", required = false)
private String tableLogicalName;
@McpParameter(description = "스키마/소유자명 (예: DAPADM, SHLOWN)", required = false)
private String owner;
}

View File

@@ -0,0 +1,37 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
* @className MetaTableResponse
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MetaTableResponse {
private List<MetaTableItem> tableList;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class MetaTableItem {
private String owner;
private String tableName;
private String tableLogicalName;
private String tableDesc;
private Integer columnCount;
private Long rowCount;
}
}

View File

@@ -5,6 +5,7 @@ import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import lombok.Getter;
import lombok.Setter;
@@ -30,8 +31,10 @@ import lombok.Setter;
public class VacationRegisterRequest {
@McpParameter(description = "연차를 등록할 사원 번호", required = true)
@McpValidation(pattern = "\\S")
private String employeeId;
@McpParameter(description = "휴가 일자 (YYYY-MM-DD 형식)", required = true)
@McpValidation(pattern = "^\\d{4}-\\d{2}-\\d{2}$")
private String date;
}
}

View File

@@ -11,6 +11,6 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
public interface BillingProcessUseCase {
Object getStatus(BillingStatusRequest req);
@McpFunction(register = false, displayName = "process 툴", name = "process", description = "청구 처리", prompt = "현재 접수된 청구건에 대한 심사 처리를 진행해.", mappingId = "BILL_002")
@McpFunction(register = false, displayName = "process 툴", name = "billing_process", description = "청구 처리", prompt = "현재 접수된 청구건에 대한 심사 처리를 진행해.", mappingId = "BILL_002")
Object processBilling(BillingProcessRequest data);
}

View File

@@ -11,6 +11,6 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
public interface BondIssueUseCase {
Object check(BondCheckRequest req);
@McpFunction(displayName = "issue 툴", name = "issue", register = false, description = "증권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002")
@McpFunction(displayName = "issue 툴", name = "bond_issue", register = false, description = "증권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002")
Object issue(BondIssueRequest data);
}

View File

@@ -11,6 +11,6 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
public interface CustomerInfoUseCase {
Object getGrade(CustomerGradeRequest req);
@McpFunction(register = false, displayName = "detail 툴", name = "detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002")
@McpFunction(register = false, displayName = "detail 툴", name = "customer_detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002")
Object getDetail(CustomerDetailRequest data);
}

View File

@@ -0,0 +1,37 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className MetaCommonCodeUseCase
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@McpTool(
routingType = "MCI",
categoryKey = "cmm"
)
public interface MetaCommonCodeUseCase {
@McpFunction(
displayName = "메타 통합코드 조회 툴",
name = "metaCommonCode",
description = "메타 통합코드 목록을 조회해줘",
prompt = "메타 통합코드 목록을 조회해줘",
mappingId = "CLCNNB00001",
register = false,
requiresApproval = false,
openWorldHint = true
)
Object execute(MetaCommonCodeRequest req);
}

View File

@@ -0,0 +1,37 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className MetaTableUseCase
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@McpTool(
routingType = "MCI",
categoryKey = "cmm"
)
public interface MetaTableUseCase {
@McpFunction(
displayName = "메타 테이블 조회 툴",
name = "metaTable",
description = "메타 테이블 정보 목록을 조회해줘",
prompt = "메타 테이블 정보 목록을 조회해줘",
mappingId = "CLCNNB00001",
register = false,
requiresApproval = false,
openWorldHint = true
)
Object execute(MetaTableRequest req);
}

View File

@@ -4,6 +4,8 @@ import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
import java.util.Map;
@McpTool(
routingType = "HTTP",
categoryKey = "cmm"
@@ -15,5 +17,5 @@ public interface TemplateUtilityUseCase {
description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.",
prompt = "요청하신 템플릿 양식 파일 다운로드 URL은 다음과 같습니다. 클릭하여 다운로드하세요:"
)
java.util.Map<String, Object> getTemplateFileUrl(TemplateDownloadRequest req);
Map<String, Object> getTemplateFileUrl(TemplateDownloadRequest req);
}

View File

@@ -0,0 +1,89 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.converter.MetaCommonCodeConverter;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeResponse;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeResponse.MetaCommonCodeItem;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.MetaCommonCodeUseCase;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl
* @className MetaCommonCodeUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MetaCommonCodeUseCaseImpl implements MetaCommonCodeUseCase {
private final MciCfpaClient mci;
private final MetaCommonCodeConverter converter;
@Override
public Object execute(MetaCommonCodeRequest req) {
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaCommonCode", req);
try {
CLCNNB00001_I mciRequest = converter.toLegacyRequest(req);
Object mciResponse = mci.callCfpa0001(mciRequest);
log.info("[MCI Tool] CLCNNB00001 MCI call completed. Returning response status: {}",
mciResponse != null);
// 현업 테스트용으로 MCI 통신을 바이패스하고 하드코딩된 메타 통합코드 샘플 결과를 반환합니다.
MetaCommonCodeResponse res = new MetaCommonCodeResponse();
List<MetaCommonCodeItem> list = new ArrayList<>();
MetaCommonCodeItem item1 = new MetaCommonCodeItem();
item1.setGroupCode(req.getGroupCode() != null ? req.getGroupCode() : "GRP_COMM_CD");
item1.setCode("CD001");
item1.setCodeName("진행중");
item1.setCodeDesc("SR 요청 처리 진행 중 상태");
item1.setSortSeq(1);
item1.setUseYn("Y");
list.add(item1);
MetaCommonCodeItem item2 = new MetaCommonCodeItem();
item2.setGroupCode(req.getGroupCode() != null ? req.getGroupCode() : "GRP_COMM_CD");
item2.setCode("CD002");
item2.setCodeName("완료");
item2.setCodeDesc("SR 요청 처리 완료 상태");
item2.setSortSeq(2);
item2.setUseYn("Y");
list.add(item2);
MetaCommonCodeItem item3 = new MetaCommonCodeItem();
item3.setGroupCode(req.getGroupCode() != null ? req.getGroupCode() : "GRP_COMM_CD");
item3.setCode("CD003");
item3.setCodeName("보류");
item3.setCodeDesc("SR 요청 처리 일시 보류 상태");
item3.setSortSeq(3);
item3.setUseYn("N");
list.add(item3);
res.setCodeList(list);
return res;
} catch (Exception e) {
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
}
}
}

View File

@@ -0,0 +1,89 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.converter.MetaTableConverter;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableResponse;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableResponse.MetaTableItem;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.MetaTableUseCase;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl
* @className MetaTableUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author 09863409
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 09863409 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MetaTableUseCaseImpl implements MetaTableUseCase {
private final MciCfpaClient mci;
private final MetaTableConverter converter;
@Override
public Object execute(MetaTableRequest req) {
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaTable", req);
try {
CLCNNB00001_I mciRequest = converter.toLegacyRequest(req);
Object mciResponse = mci.callCfpa0001(mciRequest);
log.info("[MCI Tool] CLCNNB00001 MCI call completed. Returning response status: {}",
mciResponse != null);
// 현업 테스트용으로 MCI 통신을 바이패스하고 하드코딩된 메타 테이블 샘플 결과를 반환합니다.
MetaTableResponse res = new MetaTableResponse();
List<MetaTableItem> list = new ArrayList<>();
MetaTableItem item1 = new MetaTableItem();
item1.setOwner(req.getOwner() != null ? req.getOwner() : "DAPADM");
item1.setTableName(req.getTableName() != null ? req.getTableName() : "TB_CUST_BAS");
item1.setTableLogicalName("고객기본정보");
item1.setTableDesc("고객 기본 프로필 및 인적사항 관리 테이블");
item1.setColumnCount(35);
item1.setRowCount(1250000L);
list.add(item1);
MetaTableItem item2 = new MetaTableItem();
item2.setOwner(req.getOwner() != null ? req.getOwner() : "DAPADM");
item2.setTableName("TB_CONT_MCD");
item2.setTableLogicalName("계약주계약정보");
item2.setTableDesc("보험 계약 주계약 상세 원장 테이블");
item2.setColumnCount(58);
item2.setRowCount(3400000L);
list.add(item2);
MetaTableItem item3 = new MetaTableItem();
item3.setOwner(req.getOwner() != null ? req.getOwner() : "DAPADM");
item3.setTableName("TB_CLAIM_DTL");
item3.setTableLogicalName("청구접수상세");
item3.setTableDesc("보험금 청구 접수 건별 내역 테이블");
item3.setColumnCount(42);
item3.setRowCount(890000L);
list.add(item3);
res.setTableList(list);
return res;
} catch (Exception e) {
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
}
}
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.dap.mcc.biz.oth.converter;
import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import org.mapstruct.Mapper;
/**
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.converter
* @className Onnba3011Converter
* @description Converts ONNBA tool input into the CLCNNB00001 MCI payload
* @author 0986406
* @create 2026.07.27
* <pre>
* ---------- revision history ----------
* date author description
* ---------- --------- ---------------------------
* 2026.07.27 0986406 initial creation
* </pre>
*/
@Mapper(componentModel = "spring")
public interface Onnba3011Converter {
CLCNNB00001_I toMciRequest(Onnba3011Request source);
CLCNNB00001_I.UnfcPrbuIrcoAdduDto toUnfcPrbuIrcoAddu(
Onnba3011Request.UnfcPrbuIrcoAdduDto source);
CLCNNB00001_I.SucoIspaBasDto toSucoIspaBas(Onnba3011Request.SucoIspaBasDto source);
}

View File

@@ -5,6 +5,6 @@ import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.mcc.biz.oth.dto.*;
@McpTool(routingType = "MCI", categoryKey = "oth")
public interface OnnbaMciToolUseCase {
public interface Onnba3011UseCase {
Object callOnnba3011(Onnba3011Request req);
}

View File

@@ -1,7 +1,9 @@
package io.shinhanlife.dap.mcc.biz.oth.usecase.impl;
import io.shinhanlife.dap.mcc.biz.oth.converter.Onnba3011Converter;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient;
import io.shinhanlife.dap.mcc.biz.oth.usecase.OnnbaMciToolUseCase;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import io.shinhanlife.dap.mcc.biz.oth.usecase.Onnba3011UseCase;
import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -24,13 +26,13 @@ import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class OnnbaMciToolUseCaseImpl implements OnnbaMciToolUseCase {
public class Onnba3011UseCaseImpl implements Onnba3011UseCase {
private static final String INTERFACE_CODE_3011 = "CLCNNB00001";
// 공통 MCI Client 주입
private final MciCfpaClient mciCfpaClient;
private final Onnba3011Converter onnba3011Converter;
/**
* AI Agent가 호출하게 메서드입니다.
* @McpFunction 어노테이션 하나로 AI 도구로 자동 노출 라우팅됩니다.
@@ -42,7 +44,8 @@ public class OnnbaMciToolUseCaseImpl implements OnnbaMciToolUseCase {
try {
// MciCfpaClient를 통한 호출
Object response = mciCfpaClient.callCfpa0001(req);
CLCNNB00001_I mciRequest = onnba3011Converter.toMciRequest(req);
Object response = mciCfpaClient.callCfpa0001(mciRequest);
log.info("[MCI Tool] Glow 기반 MCI 연동 성공.");

View File

@@ -0,0 +1,26 @@
package io.shinhanlife.dap.mcc.biz.smp.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Data;
/**
* @package io.shinhanlife.dap.mcc.biz.smp.dto
* @className TeamMemberRequest
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TeamMemberRequest {
@McpParameter(description = "조회할 팀 이름 (예: AX, MCP, TOOL, 전체 등)", required = false)
private String teamName;
}

View File

@@ -0,0 +1,24 @@
package io.shinhanlife.dap.mcc.biz.smp.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package io.shinhanlife.dap.mcc.biz.smp.dto
* @className TeamMemberResponse
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TeamMemberResponse {
private String result;
}

View File

@@ -0,0 +1,23 @@
package io.shinhanlife.dap.mcc.biz.smp.usecase;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest;
@McpTool(
routingType = "DIRECT",
categoryKey = "smp"
)
public interface TeamMemberUseCase {
@McpFunction(
displayName = "신한라이프 MCP, TOOL 파트 구성원 조회",
name = "get_smp_members",
description = "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.",
prompt = "신한라이프 MCP, TOOL 파트 구성원을 조회해 줘. (주의: 응답 시 ** 등 마크다운 기호를 절대 사용하지 말고 평문으로만 출력해 줘)",
mappingId = "DIRECT0001",
register = false,
requiresApproval = false,
openWorldHint = true
)
Object execute(TeamMemberRequest req);
}

View File

@@ -0,0 +1,54 @@
package io.shinhanlife.dap.mcc.biz.smp.usecase.impl;
import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest;
import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberResponse;
import io.shinhanlife.dap.mcc.biz.smp.usecase.TeamMemberUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @package io.shinhanlife.dap.mcc.biz.smp.usecase.impl
* @className TeamMemberUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TeamMemberUseCaseImpl extends AbstractMcpToolUseCase implements TeamMemberUseCase {
@Override
public Object execute(TeamMemberRequest req) {
log.info("[A01] 신한라이프 MCP, TOOL 파트 구성원 조회 요청: {}", req);
String filter = req != null && req.getTeamName() != null ? req.getTeamName().toUpperCase() : "전체";
String resultString = "";
if (filter.contains("AX")) {
resultString += "ax 추진팀 박세진 프로\n";
} else if (filter.contains("MCP") && !filter.contains("TOOL")) {
resultString += "MCP 팀은 고석민 수석 , 장효원 책임\n";
} else if (filter.contains("TOOL") && !filter.contains("MCP")) {
resultString += "TOOL 팀은 김형식 수석 ,김영진 책임 , 김도겸 대리 , 이주희 선임 , 문주현 선임 , 이보람 대리 , 박수빈 대리\n";
} else {
resultString += "ax 추진팀 박세진 프로\n" +
"MCP & TOOL 팀 담당자는 윤희준 이사\n" +
"MCP 팀은 고석민 수석 , 장효원 책임\n" +
"TOOL 팀은 김형식 수석 ,김영진 책임 , 김도겸 대리 , 이주희 선임 , 문주현 선임 , 이보람 대리 , 박수빈 대리";
}
TeamMemberResponse res = new TeamMemberResponse();
res.setResult(resultString.trim());
return res;
}
}

View File

@@ -0,0 +1,33 @@
package io.shinhanlife.dap.mcc.biz.sol.converter;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListResponse;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_O;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @package io.shinhanlife.dap.mcc.biz.sol.converter
* @className SolReqListConverter
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public interface SolReqListConverter {
@Mapping(source = "status", target = "reqStatus", defaultValue = "진행중")
@Mapping(source = "period", target = "reqPeriod", defaultValue = "최근 3개월")
@Mapping(source = "target", target = "reqTarget", defaultValue = "나의 업무")
SOLG00000001_I toLegacyRequest(SolReqListRequest req);
SolReqListResponse toResponse(SOLG00000001_O mciRes);
}

View File

@@ -0,0 +1,32 @@
package io.shinhanlife.dap.mcc.biz.sol.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Data;
/**
* @package io.shinhanlife.dap.mcc.biz.sol.dto
* @className SolReqListRequest
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SolReqListRequest {
@McpParameter(description = "진행상태 (예: 진행중, 완료 등)", required = false)
private String status;
@McpParameter(description = "조회기간 (예: 1개월, 3개월 등)", required = false)
private String period;
@McpParameter(description = "조회대상 (예: 나의 업무, 전체 등)", required = false)
private String target;
}

View File

@@ -0,0 +1,37 @@
package io.shinhanlife.dap.mcc.biz.sol.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;
/**
* @package io.shinhanlife.dap.mcc.biz.sol.dto
* @className SolReqListResponse
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SolReqListResponse {
private List<SolReqListItem> reqList;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class SolReqListItem {
private String srId;
private String srName;
private String process;
private String devStage;
private String appName;
private String requester;
}
}

View File

@@ -0,0 +1,23 @@
package io.shinhanlife.dap.mcc.biz.sol.usecase;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest;
@McpTool(
routingType = "MCI",
categoryKey = "sol"
)
public interface SolReqListUseCase {
@McpFunction(
displayName = "SolReqList 툴",
name = "solReqList",
description = "SOL 의뢰서 목록 조회해줘",
prompt = "SOL 의뢰서 목록 조회해줘",
mappingId = "SOLG00000001",
register = false,
requiresApproval = false,
openWorldHint = true
)
Object execute(SolReqListRequest req);
}

View File

@@ -0,0 +1,80 @@
package io.shinhanlife.dap.mcc.biz.sol.usecase.impl;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListResponse;
import io.shinhanlife.dap.mcc.biz.sol.usecase.SolReqListUseCase;
import io.shinhanlife.glow.communication.dto.Transfer;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import io.shinhanlife.dap.mcc.biz.sol.converter.SolReqListConverter;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_O;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.MciNclgClient;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListResponse.SolReqListItem;
/**
* @package io.shinhanlife.dap.mcc.biz.sol.usecase.impl
* @className SolReqListUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SolReqListUseCaseImpl implements SolReqListUseCase {
private final MciNclgClient mci;
private final SolReqListConverter converter;
@Override
public Object execute(SolReqListRequest req) {
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "solReqList", req);
try {
SOLG00000001_I mciRequest = converter.toLegacyRequest(req);
Transfer<SOLG00000001_O> mciResponse = mci.callTo(
"SOLG00000001", "SOLG00000001", mciRequest, SOLG00000001_O.class);
log.info("[MCI Tool] SOLG00000001 MCI call completed. Returning dummy response: {}",
mciResponse != null);
// 현업 테스트용으로 MCI 통신을 바이패스하고 하드코딩된 결과를 반환합니다.
SolReqListResponse res = new SolReqListResponse();
List<SolReqListItem> list = new ArrayList<>();
SolReqListItem item1 = new SolReqListItem();
item1.setSrId("SR-2026-001");
item1.setSrName("AX HUB 메인 화면 UI 개편");
item1.setProcess("진행중");
item1.setDevStage("개발(단위테스트)");
item1.setAppName("AX HUB");
item1.setRequester("윤희준");
list.add(item1);
SolReqListItem item2 = new SolReqListItem();
item2.setSrId("SR-2026-002");
item2.setSrName("툴 연동 모듈 추가 개발");
item2.setProcess("진행중");
item2.setDevStage("분석/설계");
item2.setAppName("MCP Gateway");
item2.setRequester("고석민");
list.add(item2);
res.setReqList(list);
return res;
} catch (Exception e) {
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
}
}
}

View File

@@ -3,7 +3,6 @@ package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_O;
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request;
import io.shinhanlife.glow.communication.dto.Transfer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -48,39 +47,8 @@ public class MciCfpaClient {
* @param inDto 입력 DTO
* @return 출력 객체
*/
public Object callCfpa0001(Onnba3011Request inDto) throws Exception {
public Object callCfpa0001(CLCNNB00001_I mciReq) throws Exception {
// 인터페이스 IO 객체 생성 및 매핑
CLCNNB00001_I mciReq = new CLCNNB00001_I();
mciReq.setDalScCd(inDto.getDalScCd());
mciReq.setCstSucoRltyCd(inDto.getCstSucoRltyCd());
mciReq.setCsNo(inDto.getCsNo());
mciReq.setRdreNo(inDto.getRdreNo());
mciReq.setUnfcPvsCalReqYn(inDto.getUnfcPvsCalReqYn());
mciReq.setKcisPymmTnnrRequest(inDto.getKcisPymmTnnrRequest());
mciReq.setLmovYn(inDto.getLmovYn());
mciReq.setGenPsthApvTrgtYn(inDto.getGenPsthApvTrgtYn());
mciReq.setIrcoLmovEcpbTrgtYn(inDto.getIrcoLmovEcpbTrgtYn());
mciReq.setDigCalYn(inDto.getDigCalYn());
mciReq.setPrbuIciDigCalYn(inDto.getPrbuIciDigCalYn());
if (inDto.getUnfcPrbuIrcoAddu() != null) {
mciReq.setUnfcPrbuIrcoAddu(new CLCNNB00001_I.UnfcPrbuIrcoAdduDto());
}
if (inDto.getSucoIspaBasDto() != null) {
CLCNNB00001_I.SucoIspaBasDto sucoIspa = new CLCNNB00001_I.SucoIspaBasDto();
sucoIspa.setCcnDalTypCd(inDto.getSucoIspaBasDto().getCcnDalTypCd());
sucoIspa.setNwcnptCursCd(inDto.getSucoIspaBasDto().getNwcnptCursCd());
sucoIspa.setInduAsctScCd(inDto.getSucoIspaBasDto().getInduAsctScCd());
sucoIspa.setCepeOgnzNo(inDto.getSucoIspaBasDto().getCepeOgnzNo());
sucoIspa.setCepePrafNo(inDto.getSucoIspaBasDto().getCepePrafNo());
sucoIspa.setClmoOgnzNo(inDto.getSucoIspaBasDto().getClmoOgnzNo());
sucoIspa.setClmoPrafNo(inDto.getSucoIspaBasDto().getClmoPrafNo());
sucoIspa.setSucoYmd(inDto.getSucoIspaBasDto().getSucoYmd());
sucoIspa.setIspDt(inDto.getSucoIspaBasDto().getIspDt());
sucoIspa.setContYmd(inDto.getSucoIspaBasDto().getContYmd());
mciReq.setSucoIspaBasDto(sucoIspa);
}
// 인터페이스 호출
Transfer<CLCNNB00001_O> resTransfer = mci.callTo(INTERFACE_CODE, mciReq, CLCNNB00001_O.class);

View File

@@ -0,0 +1,30 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
/**
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g
* @className MciNclgClient
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Component
@RequiredArgsConstructor
public class MciNclgClient {
private final AxhubMciComponent mci;
public <T> Transfer<T> callTo(String interfaceId, String dummy, Object mciReq, Class<T> resType) throws Exception {
return mci.callTo(interfaceId, dummy, mciReq, resType);
}
}

View File

@@ -0,0 +1,24 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io;
import lombok.Data;
/**
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io
* @className SOLG00000001_I
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Data
public class SOLG00000001_I {
private String reqStatus;
private String reqPeriod;
private String reqTarget;
}

View File

@@ -0,0 +1,34 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io;
import lombok.Data;
import java.util.List;
/**
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io
* @className SOLG00000001_O
* @description AX HUB 시스템 처리 클래스
* @author jade
* @create 2026.07.29
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.07.29 jade 최초생성
*
* </pre>
*/
@Data
public class SOLG00000001_O {
private List<SOLG00000001_O_Item> reqList;
@Data
public static class SOLG00000001_O_Item {
private String srId;
private String srName;
private String process;
private String devStage;
private String appName;
private String requester;
}
}

View File

@@ -0,0 +1,46 @@
package io.shinhanlife.dap.mcc.biz.oth.usecase.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.shinhanlife.dap.mcc.biz.oth.converter.Onnba3011Converter;
import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
/**
* @package io.shinhanlife.dap.mcc.biz.oth.usecase.impl
* @className OnnbaMciToolUseCaseImplTest
* @description ONNBA MCI tool use case test
* @author 0986406
* @create 2026.07.27
* <pre>
* ---------- revision history ----------
* date author description
* ---------- --------- ---------------------------
* 2026.07.27 0986406 initial creation
* </pre>
*/
class Onnba3011UseCaseImplTest {
@Test
void convertsToolRequestBeforeCallingMciClient() throws Exception {
MciCfpaClient mciCfpaClient = Mockito.mock(MciCfpaClient.class);
Onnba3011Converter converter = Mockito.mock(Onnba3011Converter.class);
Onnba3011UseCaseImpl useCase = new Onnba3011UseCaseImpl(mciCfpaClient, converter);
Onnba3011Request request = new Onnba3011Request();
CLCNNB00001_I mciRequest = new CLCNNB00001_I();
when(converter.toMciRequest(request)).thenReturn(mciRequest);
when(mciCfpaClient.callCfpa0001(mciRequest)).thenReturn("success");
Object result = useCase.callOnnba3011(request);
assertEquals("success", result);
verify(converter).toMciRequest(request);
verify(mciCfpaClient).callCfpa0001(mciRequest);
}
}

View File

@@ -0,0 +1,39 @@
package io.shinhanlife.dap.mcc.biz.sol.usecase.impl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.shinhanlife.dap.mcc.biz.sol.converter.SolReqListConverter;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListResponse;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.MciNclgClient;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000001_O;
import io.shinhanlife.glow.communication.dto.Transfer;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
class SolReqListUseCaseImplTest {
@Test
void callsMciWithConvertedRequestAndReturnsDummyResponse() throws Exception {
MciNclgClient mci = Mockito.mock(MciNclgClient.class);
SolReqListConverter converter = Mockito.mock(SolReqListConverter.class);
SolReqListUseCaseImpl useCase = new SolReqListUseCaseImpl(mci, converter);
SolReqListRequest request = new SolReqListRequest();
SOLG00000001_I mciRequest = new SOLG00000001_I();
when(converter.toLegacyRequest(request)).thenReturn(mciRequest);
when(mci.callTo(eq("SOLG00000001"), eq("SOLG00000001"), eq(mciRequest), eq(SOLG00000001_O.class)))
.thenReturn(new Transfer<>());
SolReqListResponse response = (SolReqListResponse) useCase.execute(request);
verify(converter).toLegacyRequest(request);
verify(mci).callTo("SOLG00000001", "SOLG00000001", mciRequest, SOLG00000001_O.class);
assertThat(response.getReqList()).extracting(SolReqListResponse.SolReqListItem::getSrId)
.containsExactly("SR-2026-001", "SR-2026-002");
}
}

View File

@@ -0,0 +1,55 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.converter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import io.shinhanlife.dap.mcc.biz.oth.converter.Onnba3011Converter;
import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
/**
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.converter
* @className Onnba3011MciRequestConverterTest
* @description ONNBA 3011 request converter test
* @author 0986406
* @create 2026.07.27
* <pre>
* ---------- revision history ----------
* date author description
* ---------- --------- ---------------------------
* 2026.07.27 0986406 initial creation
* </pre>
*/
class Onnba3011MciRequestConverterTest {
private final Onnba3011Converter converter = Mappers.getMapper(Onnba3011Converter.class);
@Test
void mapsRootAndNestedRequestFieldsToMciPayload() {
Onnba3011Request source = request();
CLCNNB00001_I result = converter.toMciRequest(source);
assertEquals("A", result.getDalScCd());
assertEquals("123456", result.getCsNo());
assertNotNull(result.getUnfcPrbuIrcoAddu());
assertNotNull(result.getSucoIspaBasDto());
assertEquals("01", result.getSucoIspaBasDto().getCcnDalTypCd());
assertEquals("20260727", result.getSucoIspaBasDto().getSucoYmd());
}
private Onnba3011Request request() {
Onnba3011Request request = new Onnba3011Request();
request.setDalScCd("A");
request.setCsNo("123456");
request.setUnfcPrbuIrcoAddu(new Onnba3011Request.UnfcPrbuIrcoAdduDto());
Onnba3011Request.SucoIspaBasDto suco = new Onnba3011Request.SucoIspaBasDto();
suco.setCcnDalTypCd("01");
suco.setSucoYmd("20260727");
request.setSucoIspaBasDto(suco);
return request;
}
}

View File

@@ -1,19 +1,8 @@
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY dap-tool-core dap-tool-core
COPY dap-tool-sms dap-tool-sms
RUN chmod +x gradlew
RUN ./gradlew clean :dap-tool-sms:build -x test
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
COPY --from=builder /app/dap-tool-sms/build/libs/*-SNAPSHOT.jar app.jar
COPY dap-tool-sms/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8082
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -5,6 +5,7 @@ import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import lombok.Getter;
import lombok.Setter;
@@ -30,10 +31,12 @@ import lombok.Setter;
public class SmsSendRequest {
@McpParameter(description = "수신자 전화번호", required = true)
@McpValidation(pattern = "^01[0-9]-?\\d{3,4}-?\\d{4}$")
private String phoneNumber;
@McpParameter(description = "전송할 메시지 내용", required = true)
@McpValidation(pattern = "\\S")
private String message;
}
}

View File

@@ -1,34 +0,0 @@
$ErrorActionPreference = "Stop"
$extensions = @("*.java", "*.xml", "*.yml", "*.properties", "*.gradle", "*.md")
$baseDir = "C:\eGovFrameDev-4.3.1-64bit\workspace-egov\axhub-backend-main"
Write-Host "Replacing text (Req -> Request, Res -> Response)..."
Get-ChildItem -Path $baseDir -Include $extensions -Recurse -File | ForEach-Object {
if ($_.FullName -notmatch "\\build\\" -and $_.FullName -notmatch "\\\.git\\" -and $_.FullName -notmatch "\\\.gradle\\" -and $_.FullName -notmatch "\\bin\\") {
try {
$content = Get-Content $_.FullName -Raw -Encoding UTF8
$newContent = $content -creplace "Req\b", "Request"
$newContent = $newContent -creplace "Res\b", "Response"
if ($content -cne $newContent) {
Write-Host "Updated text in: $($_.FullName)"
$utf8NoBom = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::WriteAllText($_.FullName, $newContent, $utf8NoBom)
}
} catch {
Write-Host "Skipped binary or locked file: $($_.FullName)"
}
}
}
Write-Host "Renaming DTO files..."
Get-ChildItem -Path $baseDir -Include *Req.java,*Res.java -Recurse -File | ForEach-Object {
if ($_.FullName -notmatch "\\build\\" -and $_.FullName -notmatch "\\\.git\\" -and $_.FullName -notmatch "\\\.gradle\\" -and $_.FullName -notmatch "\\bin\\") {
$newName = $_.Name -creplace "Req\.java$", "Request.java"
$newName = $newName -creplace "Res\.java$", "Response.java"
Write-Host "Renaming file: $($_.FullName) -> $newName"
Rename-Item -Path $_.FullName -NewName $newName
}
}
Write-Host "DTO Refactoring completed!"

View File

@@ -1,33 +0,0 @@
$ErrorActionPreference = "Stop"
$extensions = @("*.java", "*.xml", "*.yml", "*.properties", "*.gradle", "*.md", "Dockerfile*")
$baseDir = "C:\eGovFrameDev-4.3.1-64bit\workspace-egov\axhub-backend-main"
Write-Host "Replacing text..."
Get-ChildItem -Path $baseDir -Include $extensions -Recurse -File | ForEach-Object {
if ($_.FullName -notmatch "\\build\\" -and $_.FullName -notmatch "\\\.git\\" -and $_.FullName -notmatch "\\\.gradle\\" -and $_.FullName -notmatch "\\bin\\") {
try {
$content = Get-Content $_.FullName -Raw -Encoding UTF8
$newContent = $content -replace "dapms", "mcg"
$newContent = $newContent -replace "dapmt", "mcc"
if ($content -cne $newContent) {
Write-Host "Updated text in: $($_.FullName)"
$utf8NoBom = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::WriteAllText($_.FullName, $newContent, $utf8NoBom)
}
} catch {
Write-Host "Skipped binary or locked file: $($_.FullName)"
}
}
}
Write-Host "Renaming directories..."
Get-ChildItem -Path $baseDir -Recurse -Directory | Where-Object { $_.Name -eq "dapms" -or $_.Name -eq "dapmt" } | Sort-Object -Property @{Expression={$_.FullName.Length}; Descending=$true} | ForEach-Object {
if ($_.FullName -notmatch "\\build\\" -and $_.FullName -notmatch "\\\.git\\" -and $_.FullName -notmatch "\\\.gradle\\" -and $_.FullName -notmatch "\\bin\\") {
$newName = if ($_.Name -eq "dapms") { "mcg" } else { "mcc" }
Write-Host "Renaming dir: $($_.FullName) -> $newName"
Rename-Item -Path $_.FullName -NewName $newName
}
}
Write-Host "Refactoring completed!"