2 Commits

Author SHA1 Message Date
mjh
05ea5f9aaa merge: feature/20260805_mjh into feature/20260805_emis_create 2026-08-05 09:41:09 +09:00
mjh
50315ad245 feat: add ONCSC1340 MCI client and IO DTOs 2026-08-05 09:33:58 +09:00
558 changed files with 13997 additions and 28184 deletions

View File

@@ -33,11 +33,7 @@
* 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다.
* 신한라이프 표준 로그 기준 (Logback 설정):
* **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dat-gateway/A01/`)
* **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dap-gateway/A01/`)
* **로그 네이밍 규칙:** `${HOSTNAME}_코드명_yyyyMMdd.log` (예: `${HOSTNAME}_A01_20260722.log`)
* **기본 로그 구분 코드:** 시스템 운영기록 가동기록의 경우 `A01`을 기본으로 사용한다.
* **호스트명 동적 할당:** Logback 설정 시 `<property name="HOSTNAME" value="${HOSTNAME}" />` 를 선언하여 사용한다.
* 프로젝트 작업 범위 및 툴 연동 규칙:
* 현재 `axhub-backend-main` 워크스페이스 내에서는 **오직 `dat-gateway` 모듈만 수정**한다.
* 툴(Tool) 연동 개발 작업은 반드시 별도의 워크스페이스인 **`C:\eGovFrameDev-4.3.1-64bit\workspace-egov\dat-was-datmt`** 폴더에서 수행한다.

View File

@@ -18,7 +18,7 @@ jobs:
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 docker-cli docker-cli-compose
apk add --no-cache nodejs git openjdk21 rsync
fi
- name: Checkout Code
@@ -28,7 +28,7 @@ jobs:
run: |
echo "Copying latest code to /app (Host Volume) and removing stale files..."
if command -v rsync &> /dev/null; then
rsync -a --delete --exclude='.git' --exclude='bin' --exclude='act_runner_data' . /app/
rsync -a --delete --exclude='.git' . /app/
else
rm -rf /app/dap-* /app/src /app/build.gradle /app/settings.gradle
cp -a . /app/
@@ -59,10 +59,9 @@ jobs:
# 4. 마운트된 /app 디렉토리로 이동하여 호스트의 도커 컴포즈 제어!
cd /app
docker system prune -f
ACTIVE_PROFILE=dev docker compose up -d --build --pull always --remove-orphans gateway tool-report mci-mock dozzle was-sal was-cus was-pro was-sys
ACTIVE_PROFILE=dev docker compose up -d --build --remove-orphans gateway redis mci-mock dozzle was-sms was-oth
# 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 및 빌드 캐시 자동 소거 청소!
docker image prune -a -f
docker builder prune -a -f
# 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 자동 소거 청소!
docker image prune -f
echo "CI/CD Deploy Success!"

3
.gitignore vendored
View File

@@ -122,6 +122,3 @@ ehthumbs.db
# ===== ?섍꼍?ㅼ젙 (誘쇨컧?뺣낫 遺꾨━ ?? =====
# application-local.yml
# application-secret.yml
# ===== Generated Sources =====
**/src/main/generated/

View File

@@ -1,2 +0,0 @@
#Wed Jul 29 17:05:55 KST 2026
gradle.version=8.14.3

Binary file not shown.

10
.idea/.gitignore generated vendored
View File

@@ -1,10 +0,0 @@
# 디폴트 무시된 파일
/shelf/
/workspace.xml
# 에디터 기반 HTTP 클라이언트 요청
/httpRequests/
# 쿼리 파일을 포함한 무시된 디폴트 폴더
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
.idea/.name generated
View File

@@ -1 +0,0 @@
dap-admin

153
.idea/compiler.xml generated
View File

@@ -1,153 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Gradle Imported" enabled="true">
<outputRelativeToContentRoot value="true" />
<option name="mapstruct.defaultComponentModel" value="spring" />
<processorPath useClasspath="false">
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.42/8365263844ebb62398e0dc33057ba10ba472d3b8/lombok-1.18.42.jar" />
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.mapstruct/mapstruct-processor/1.6.3/52e345c907fbf173b376586c7f8b131d53fa5867/mapstruct-processor-1.6.3.jar" />
</processorPath>
<module name="dap-admin.dat-was-pro.main" />
<module name="dap-admin.dat-was-cus.main" />
<module name="dap-admin.dat-was-sys.main" />
<module name="dap-admin.dat-was-lib.main" />
<module name="dap-admin.dap-tool-report.main" />
<module name="dap-admin.dat-gateway.main" />
<module name="dap-admin.dat-was-sal.main" />
</profile>
<profile name="Gradle Imported" enabled="true">
<outputRelativeToContentRoot value="true" />
<option name="mapstruct.defaultComponentModel" value="spring" />
<processorPath useClasspath="false">
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.42/8365263844ebb62398e0dc33057ba10ba472d3b8/lombok-1.18.42.jar" />
</processorPath>
<module name="dap-admin.dat-was-cus.test" />
<module name="dap-admin.dat-was-sal.test" />
<module name="dap-admin.dap-tool-report.test" />
<module name="dap-admin.dat-was-sys.test" />
<module name="dap-admin.dat-gateway.test" />
<module name="dap-admin.dat-was-pro.test" />
<module name="dap-admin.dat-was-lib.test" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel target="21" />
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="axhub-admin.axhub-common" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-common.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-common.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-gateway" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-gateway.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-gateway.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-core" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-core.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-core.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-email" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-email.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-email.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-hr" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-hr.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-hr.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-other" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-other.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-other.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-payment" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-payment.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-payment.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-sms" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-sms.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="axhub-admin.axhub-tool-sms.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin" options="-parameters" />
<module name="dap-admin.dap-common" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-common.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-common.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-core" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-core.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-core.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-email" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-email.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-email.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-hr" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-hr.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-hr.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-oth" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-oth.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-oth.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-other" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-other.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-other.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-payment" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-payment.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-payment.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-report" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-report.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-report.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-sms" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-sms.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dap-tool-sms.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-gateway" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-gateway.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-gateway.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-tool-report" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-tool-report.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-tool-report.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-cus" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-cus.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-cus.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-lib" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-lib.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-lib.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-oth" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-oth.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-oth.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-pro" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-pro.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-pro.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-sal" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-sal.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-sal.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-sms" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-sms.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-sms.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-sys" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-sys.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.dat-was-sys.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dap-admin.main" options="-parameters" />
<module name="dap-admin.test" options="-parameters" />
<module name="dat-admin.dat-gateway" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-gateway.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-gateway.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-tool-report" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-tool-report.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-tool-report.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-cus" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-cus.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-cus.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-lib" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-lib.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-lib.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-pro" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-pro.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-pro.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-sal" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-sal.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-sal.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-sys" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-sys.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="dat-admin.dat-was-sys.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="io.shinhanlife.dat-admin.dap-tool-core" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="io.shinhanlife.dat-admin.dap-tool-core.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="io.shinhanlife.dat-admin.dap-tool-core.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="io.shinhanlife.dat-admin.dap-tool-oth" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="io.shinhanlife.dat-admin.dap-tool-oth.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="io.shinhanlife.dat-admin.dap-tool-oth.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="io.shinhanlife.dat-admin.dap-tool-sms" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="io.shinhanlife.dat-admin.dap-tool-sms.main" options="-parameters -Amapstruct.defaultComponentModel=spring" />
<module name="io.shinhanlife.dat-admin.dap-tool-sms.test" options="-parameters -Amapstruct.defaultComponentModel=spring" />
</option>
</component>
</project>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="IU-261.25134.95">
<data-source name="MCP_Redis" uuid="e04b0a95-55ed-4687-8007-98dd44e0ab05">
<database-info product="Redis Standalone" version="8.10.0" jdbc-version="4.2" driver-name="Redis JDBC Driver" driver-version="1.5" dbms="REDIS" exact-version="8.10.0" exact-driver-version="1.5" />
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<secret-storage>master_key</secret-storage>
<auth-provider>no-auth</auth-provider>
<schema-mapping>
<introspection-scope>
<node kind="schema" qname="@" />
</introspection-scope>
</schema-mapping>
</data-source>
</component>
</project>

17
.idea/dataSources.xml generated
View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="MCP_Redis" uuid="e04b0a95-55ed-4687-8007-98dd44e0ab05">
<driver-ref>redis</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
<jdbc-url>jdbc:redis://localhost:6379/0</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -1,25 +0,0 @@
<DataSourcesHistory>
<DataSourceFromHistory isRemovedFromProject="false">
<data-source source="LOCAL" name="MCP_Redis" uuid="e04b0a95-55ed-4687-8007-98dd44e0ab05">
<database-info product="Redis Standalone" version="8.10.0" jdbc-version="4.2" driver-name="Redis JDBC Driver" driver-version="1.5" dbms="REDIS" exact-version="8.10.0" exact-driver-version="1.5" />
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<driver-ref>redis</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
<jdbc-url>jdbc:redis://localhost:6379/0</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<secret-storage>master_key</secret-storage>
<auth-provider>no-auth</auth-provider>
<schema-mapping>
<introspection-scope>
<node kind="schema" qname="@" />
</introspection-scope>
</schema-mapping>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</DataSourceFromHistory>
</DataSourcesHistory>

6
.idea/encodings.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

23
.idea/gradle.xml generated
View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/dat-gateway" />
<option value="$PROJECT_DIR$/dap-tool-report" />
<option value="$PROJECT_DIR$/dat-was-cus" />
<option value="$PROJECT_DIR$/dat-was-lib" />
<option value="$PROJECT_DIR$/dat-was-pro" />
<option value="$PROJECT_DIR$/dat-was-sal" />
<option value="$PROJECT_DIR$/dat-was-sys" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

17
.idea/misc.xml generated
View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

17
.idea/modules.xml generated
View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dap-tool-core/dap-admin.dap-tool-core.iml" filepath="$PROJECT_DIR$/.idea/modules/dap-tool-core/dap-admin.dap-tool-core.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dap-tool-oth/dap-admin.dap-tool-oth.iml" filepath="$PROJECT_DIR$/.idea/modules/dap-tool-oth/dap-admin.dap-tool-oth.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dap-tool-oth/dap-admin.dap-tool-oth.test.iml" filepath="$PROJECT_DIR$/.idea/modules/dap-tool-oth/dap-admin.dap-tool-oth.test.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dap-tool-report/dap-admin.dap-tool-report.main.iml" filepath="$PROJECT_DIR$/.idea/modules/dap-tool-report/dap-admin.dap-tool-report.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dap-tool-sms/dap-admin.dap-tool-sms.iml" filepath="$PROJECT_DIR$/.idea/modules/dap-tool-sms/dap-admin.dap-tool-sms.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dap-tool-sms/dap-admin.dap-tool-sms.test.iml" filepath="$PROJECT_DIR$/.idea/modules/dap-tool-sms/dap-admin.dap-tool-sms.test.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dat-gateway/dap-admin.dat-gateway.main.iml" filepath="$PROJECT_DIR$/.idea/modules/dat-gateway/dap-admin.dat-gateway.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dat-gateway/dap-admin.dat-gateway.test.iml" filepath="$PROJECT_DIR$/.idea/modules/dat-gateway/dap-admin.dat-gateway.test.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dat-was-lib/dap-admin.dat-was-lib.main.iml" filepath="$PROJECT_DIR$/.idea/modules/dat-was-lib/dap-admin.dat-was-lib.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/dat-was-lib/dap-admin.dat-was-lib.test.iml" filepath="$PROJECT_DIR$/.idea/modules/dat-was-lib/dap-admin.dat-was-lib.test.iml" />
</modules>
</component>
</project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../dap-tool-core/build/generated/sources/annotationProcessor/java/main">
<sourceFolder url="file://$MODULE_DIR$/../../../dap-tool-core/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
</content>
</component>
</module>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../dap-tool-core/build/generated/sources/annotationProcessor/java/test">
<sourceFolder url="file://$MODULE_DIR$/../../../dap-tool-core/build/generated/sources/annotationProcessor/java/test" isTestSource="true" generated="true" />
</content>
</component>
</module>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../dap-tool-oth/build/generated/sources/annotationProcessor/java/main">
<sourceFolder url="file://$MODULE_DIR$/../../../dap-tool-oth/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
</content>
</component>
</module>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../dap-tool-sms/build/generated/sources/annotationProcessor/java/main">
<sourceFolder url="file://$MODULE_DIR$/../../../dap-tool-sms/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
</content>
</component>
</module>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

868
.idea/workspace.xml generated
View File

@@ -1,868 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="f2db61ef-4338-4148-81d8-f16ac2f70318" name="ë³€ê²? comment="log 경로 ?˜ì " />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="EmbeddingIndexingInfo">
<option name="cachedIndexableFilesCount" value="477" />
</component>
<component name="ExternalProjectsData">
<projectState path="$PROJECT_DIR$">
<ProjectState />
</projectState>
</component>
<component name="ExternalProjectsManager">
<system id="GRADLE">
<state>
<task path="$PROJECT_DIR$">
<activation />
</task>
<projects_view>
<tree_state>
<expand />
<select />
</tree_state>
</projects_view>
</state>
</system>
</component>
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="feature/khs_0708" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitHubPullRequestSearchHistory">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;jadekimhs-creator&quot;
}
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/jadekimhs-creator/axhub-backend-main.git&quot;,
&quot;accountId&quot;: &quot;d14f3549-59c6-4914-b2fc-96612b565c83&quot;
}
}</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/axhub-tool-core/src/main/java/io/shinhanlife/axhub/biz/mcp/adapter/test/MockEimsHttpServer.java" root0="SKIP_INSPECTION" />
</component>
<component name="HttpClientOnboardingState">{
&quot;isOnboardingCommentShown&quot;: true
}</component>
<component name="KubernetesApiPersistence">{}</component>
<component name="KubernetesApiProvider">{
&quot;isMigrated&quot;: true
}</component>
<component name="MavenImportPreferences">
<option name="generalSettings">
<MavenGeneralSettings>
<option name="mavenHomeTypeForPersistence" value="WRAPPER" />
</MavenGeneralSettings>
</option>
</component>
<component name="McpProjectServerCommands">
<commands />
<urls />
</component>
<component name="NextEditCompletionFeaturesState">
<decayedCancelled>
<entry key="MS100" value="1.0" />
<entry key="MS500" value="1.0" />
<entry key="S2" value="1.0" />
<entry key="S5" value="1.0" />
<entry key="S10" value="1.0" />
<entry key="S30" value="1.0" />
<entry key="S60" value="1.0" />
<entry key="M2" value="1.0" />
<entry key="M5" value="1.0000004063583507" />
<entry key="M10" value="1.0014248804304775" />
<entry key="M15" value="1.0216507258806833" />
<entry key="M30" value="1.3290052443235425" />
<entry key="H1" value="2.282572068736398" />
<entry key="H2" value="3.532355821946708" />
<entry key="H4" value="4.558341371394377" />
<entry key="D1" value="9.094716752814406" />
<entry key="W1" value="70.60325442196253" />
</decayedCancelled>
<decayedSelected>
<entry key="MS100" value="0.0" />
<entry key="MS500" value="0.0" />
<entry key="S2" value="0.0" />
<entry key="S5" value="0.0" />
<entry key="S10" value="0.0" />
<entry key="S30" value="0.0" />
<entry key="S60" value="0.0" />
<entry key="M2" value="0.0" />
<entry key="M5" value="4.0E-323" />
<entry key="M10" value="1.1150340305311683E-161" />
<entry key="M15" value="7.191589989208289E-108" />
<entry key="M30" value="4.642712610093428E-54" />
<entry key="H1" value="3.731613880427818E-27" />
<entry key="H2" value="1.0580265938924739E-13" />
<entry key="H4" value="5.633917287885551E-7" />
<entry key="D1" value="0.2594845425222435" />
<entry key="W1" value="3.6444939982225684" />
</decayedSelected>
<decayedShown>
<entry key="MS100" value="0.0018350053695586157" />
<entry key="MS500" value="0.2836138712339885" />
<entry key="S2" value="0.729763044896567" />
<entry key="S5" value="0.881602365474992" />
<entry key="S10" value="0.9389368272013789" />
<entry key="S30" value="0.979216654035547" />
<entry key="S60" value="0.9895537651060436" />
<entry key="M2" value="0.9947631703606863" />
<entry key="M5" value="0.9979023743437856" />
<entry key="M10" value="1.00037529616311" />
<entry key="M15" value="1.0209507142357308" />
<entry key="M30" value="1.328653905977673" />
<entry key="H1" value="2.2823944051974228" />
<entry key="H2" value="3.532265686879452" />
<entry key="H4" value="4.558296332164408" />
<entry key="D1" value="9.354155020042933" />
<entry key="W1" value="74.24764635087368" />
</decayedShown>
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 0,
&quot;fromUser&quot;: false
}</component>
<component name="ProjectId" id="3Fz4p4oVzuFdzoWCnvW5rfH4F4v" />
<component name="ProjectViewState">
<option name="autoscrollFromSource" value="true" />
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Docker.docker-compose.yml.gateway: Compose ë°°í<C2B0>¬.executor&quot;: &quot;Run&quot;,
&quot;Docker.docker-compose.yml.tool-sms: Compose ë°°í<C2B0>¬.executor&quot;: &quot;Run&quot;,
&quot;Docker.docker-compose.yml: Compose ë°°í<C2B0>¬.executor&quot;: &quot;Run&quot;,
&quot;Docker.docker-compose.yml[gateway, redis, tool-email, tool-other, tool-payment, tool-sms, mci-mock, tool-hr]: Compose ë°°í<C2B0>¬.executor&quot;: &quot;Run&quot;,
&quot;Docker.docker-compose.yml[gateway, redis, tool-email, tool-other, tool-payment, tool-sms, mci-mock]: Compose ë°°í<C2B0>¬.executor&quot;: &quot;Run&quot;,
&quot;Docker.docker-compose.yml[gateway, redis, tool-email, tool-other, tool-payment, tool-sms]: Compose ë°°í<C2B0>¬.executor&quot;: &quot;Run&quot;,
&quot;Docker.docker-compose.yml[tool-sms, tool-email, gateway, tool-other]: Compose ë°°í<C2B0>¬.executor&quot;: &quot;Run&quot;,
&quot;Gradle.SampleMciMapperTest.testAiToMciMapping.executor&quot;: &quot;Debug&quot;,
&quot;Gradle.ShinhanMciSenderTest.testJsonUnwrapped.executor&quot;: &quot;Run&quot;,
&quot;Gradle.axhub-backend-main [:axhub-gateway:bootBuildImage].executor&quot;: &quot;Run&quot;,
&quot;Gradle.axhub-backend-main [:axhub-gateway:build].executor&quot;: &quot;Run&quot;,
&quot;Gradle.axhub-backend-main [bootRun --args=&amp;quot;--server.port=8082&amp;quot;].executor&quot;: &quot;Run&quot;,
&quot;Gradle.axhub-backend-main [clean build -x test].executor&quot;: &quot;Run&quot;,
&quot;Gradle.axhub-backend-main 빌드.executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;Notification.DisplayName-DoNotAsk-DatabaseConfigFileWatcher.found&quot;: &quot;???…ë<E280A6>°?´íЏ???°ì<C2B0>´?°ë² ?´ìФ 구성??발견?˜ì—ˆ?µë‹ˆ??quot;,
&quot;Notification.DoNotAsk-DatabaseConfigFileWatcher.found&quot;: &quot;true&quot;,
&quot;RequestMappingsPanelOrder0&quot;: &quot;0&quot;,
&quot;RequestMappingsPanelOrder1&quot;: &quot;1&quot;,
&quot;RequestMappingsPanelWidth0&quot;: &quot;75&quot;,
&quot;RequestMappingsPanelWidth1&quot;: &quot;75&quot;,
&quot;RunOnceActivity.MCP Project settings loaded&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;Spring Boot.AxHubAdminApplication.executor&quot;: &quot;Debug&quot;,
&quot;Spring Boot.AxHubGatewayApplication (1).executor&quot;: &quot;Run&quot;,
&quot;Spring Boot.AxHubGatewayApplication.executor&quot;: &quot;Run&quot;,
&quot;Spring Boot.AxHubToolEmailApplication.executor&quot;: &quot;Debug&quot;,
&quot;Spring Boot.AxHubToolOtherApplication.executor&quot;: &quot;Run&quot;,
&quot;Spring Boot.AxHubToolSmsApplication.executor&quot;: &quot;Debug&quot;,
&quot;Spring Boot.HrToolApplication.executor&quot;: &quot;Debug&quot;,
&quot;Spring Boot.PaymentToolApplication.executor&quot;: &quot;Debug&quot;,
&quot;codeWithMe.voiceChat.enabledByDefault&quot;: &quot;false&quot;,
&quot;com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;feature/mcp-refactoring&quot;,
&quot;junie.onboarding.icon.badge.shown&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;project.structure.last.edited&quot;: &quot;모듈&quot;,
&quot;project.structure.proportion&quot;: &quot;0.0&quot;,
&quot;project.structure.side.proportion&quot;: &quot;0.0&quot;,
&quot;run.configurations.included.in.services&quot;: &quot;true&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.lookFeel&quot;,
&quot;to.speed.mode.migration.done&quot;: &quot;true&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
},
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;redis&quot;
]
}
}</component>
<component name="RecentsManager">
<key name="CopyClassDialog.RECENTS_KEY">
<recent name="io.shinhanlife.axhub.biz.mcp.tool.service" />
<recent name="io.shinhanlife.axhub.biz.mcp.tool.sms.service" />
<recent name="io.shinhanlife.axhub.biz.mcp.tool.mapper" />
</key>
</component>
<component name="RunDashboard">
<option name="configurationTypes">
<set>
<option value="KtorApplicationConfigurationType" />
<option value="MicronautRunConfigurationType" />
<option value="QuarkusRunConfigurationType" />
<option value="SpringBootApplicationConfigurationType" />
</set>
</option>
</component>
<component name="RunManager" selected="Docker.docker-compose.yml[gateway, redis, tool-email, tool-other, tool-payment, tool-sms, mci-mock]: Compose ë°°í<C2B0>¬">
<configuration name="SampleMciMapperTest.testAiToMciMapping" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":axhub-tool-core:test" />
<option value="--tests" />
<option value="&quot;io.shinhanlife.axhub.biz.mcp.sample.mapper.SampleMciMapperTest.testAiToMciMapping&quot;" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>true</RunAsTest>
<GradleProfilingDisabled>false</GradleProfilingDisabled>
<GradleCoverageDisabled>false</GradleCoverageDisabled>
<method v="2" />
</configuration>
<configuration name="ShinhanMciSenderTest.testJsonUnwrapped" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":axhub-tool-core:test" />
<option value="--tests" />
<option value="&quot;io.shinhanlife.axhub.biz.mcp.adapter.sender.ShinhanMciSenderTest.testJsonUnwrapped&quot;" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>true</RunAsTest>
<GradleProfilingDisabled>false</GradleProfilingDisabled>
<GradleCoverageDisabled>false</GradleCoverageDisabled>
<method v="2" />
</configuration>
<configuration name="axhub-backend-main [:axhub-gateway:bootBuildImage]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":axhub-gateway:bootBuildImage" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<GradleProfilingDisabled>false</GradleProfilingDisabled>
<GradleCoverageDisabled>false</GradleCoverageDisabled>
<method v="2" />
</configuration>
<configuration name="axhub-backend-main [bootRun --args=&amp;quot;--server.port=8082&amp;quot;]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-PmainClass=io.shinhanlife.AxHubToolApplication" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="bootRun" />
<option value="--args=&amp;quot;--server.port=8082&amp;quot;" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<GradleProfilingDisabled>false</GradleProfilingDisabled>
<GradleCoverageDisabled>false</GradleCoverageDisabled>
<method v="2" />
</configuration>
<configuration name="AxHubGatewayApplication (1)" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<module name="dap-admin.dat-gateway.main" />
<option name="SPRING_BOOT_MAIN_CLASS" value="io.shinhanlife.dat.mcg.AxHubGatewayApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="AxHubToolEmailApplication (1)" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<module name="dap-admin.dap-tool-email.main" />
<option name="SPRING_BOOT_MAIN_CLASS" value="io.shinhanlife.dat.mcc.email.AxHubToolEmailApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="AxHubToolOtherApplication (1)" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<module name="dap-admin.dap-tool-other.main" />
<option name="SPRING_BOOT_MAIN_CLASS" value="io.shinhanlife.dat.mcc.other.AxHubToolOtherApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="AxHubToolSmsApplication (1)" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<module name="dap-admin.dap-tool-sms.main" />
<option name="SPRING_BOOT_MAIN_CLASS" value="io.shinhanlife.dat.mcc.sms.AxHubToolSmsApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="PaymentToolApplication (1)" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
<module name="dap-admin.dap-tool-payment.main" />
<option name="SPRING_BOOT_MAIN_CLASS" value="io.shinhanlife.dat.mcc.payment.PaymentToolApplication" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration default="true" type="docker-deploy" factoryName="docker-compose.yml" temporary="true">
<deployment type="docker-compose.yml">
<settings />
</deployment>
<method v="2" />
</configuration>
<configuration name="docker-compose.yml[gateway, redis, tool-email, tool-other, tool-payment, tool-sms, mci-mock]: Compose ë°°í<C2B0>¬" type="docker-deploy" factoryName="docker-compose.yml" temporary="true" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="commandLineOptions" value="--build" />
<option name="services">
<list>
<option value="gateway" />
<option value="redis" />
<option value="tool-email" />
<option value="tool-other" />
<option value="tool-payment" />
<option value="tool-sms" />
<option value="mci-mock" />
</list>
</option>
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2">
<option name="Gradle.BeforeRunTask" enabled="false" tasks="clean build -x test" externalProjectPath="$PROJECT_DIR$" vmOptions="" scriptParameters="" />
</method>
</configuration>
<list>
<item itemvalue="Docker.docker-compose.yml[gateway, redis, tool-email, tool-other, tool-payment, tool-sms, mci-mock]: Compose ë°°í<C2B0>¬" />
<item itemvalue="Gradle.ShinhanMciSenderTest.testJsonUnwrapped" />
<item itemvalue="Gradle.axhub-backend-main [:axhub-gateway:bootBuildImage]" />
<item itemvalue="Gradle.SampleMciMapperTest.testAiToMciMapping" />
<item itemvalue="Gradle.axhub-backend-main [bootRun --args=&amp;quot;--server.port=8082&amp;quot;]" />
<item itemvalue="Spring Boot.AxHubGatewayApplication (1)" />
<item itemvalue="Spring Boot.AxHubToolEmailApplication (1)" />
<item itemvalue="Spring Boot.AxHubToolOtherApplication (1)" />
<item itemvalue="Spring Boot.AxHubToolSmsApplication (1)" />
<item itemvalue="Spring Boot.PaymentToolApplication (1)" />
</list>
<recent_temporary>
<list>
<item itemvalue="Docker.docker-compose.yml[gateway, redis, tool-email, tool-other, tool-payment, tool-sms, mci-mock]: Compose ë°°í<C2B0>¬" />
<item itemvalue="Gradle.ShinhanMciSenderTest.testJsonUnwrapped" />
<item itemvalue="Gradle.axhub-backend-main [:axhub-gateway:bootBuildImage]" />
<item itemvalue="Gradle.SampleMciMapperTest.testAiToMciMapping" />
<item itemvalue="Gradle.axhub-backend-main [bootRun --args=&amp;quot;--server.port=8082&amp;quot;]" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-30f59d01ecdd-37e91769500f-intellij.indexing.shared.core-IU-261.25134.95" />
<option value="bundled-js-predefined-d6986cc7102b-31caf2ab9e3c-JavaScript-IU-261.25134.95" />
</set>
</attachedChunks>
</component>
<component name="StructureViewState">
<option name="selectedTab" value="?¼ë¦¬" />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="?”í<E2809D>´???‘ì—…">
<changelist id="f2db61ef-4338-4148-81d8-f16ac2f70318" name="변� comment="" />
<created>1783064745410</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1783064745410</updated>
<workItem from="1783064746565" duration="3558000" />
<workItem from="1783295622537" duration="82000" />
<workItem from="1783296277574" duration="21824000" />
<workItem from="1783381712525" duration="17941000" />
<workItem from="1783468449372" duration="71000" />
<workItem from="1783468566397" duration="22445000" />
<workItem from="1783554872446" duration="40241000" />
<workItem from="1783673983621" duration="5000" />
<workItem from="1783900184231" duration="22468000" />
<workItem from="1783988452900" duration="11918000" />
<workItem from="1784003561991" duration="2555000" />
<workItem from="1784007005258" duration="1025000" />
<workItem from="1784008697772" duration="917000" />
<workItem from="1784009797198" duration="27131000" />
<workItem from="1784159561976" duration="18127000" />
<workItem from="1784187067292" duration="1473000" />
<workItem from="1784189910429" duration="11107000" />
</task>
<task id="LOCAL-00001" summary="index.html ?ŒìФ ?˜ì •">
<option name="closed" value="true" />
<created>1783316153458</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1783316153458</updated>
</task>
<task id="LOCAL-00002" summary="?ŒìФ ?˜ì •">
<option name="closed" value="true" />
<created>1783326414753</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1783326414753</updated>
</task>
<task id="LOCAL-00003" summary="?˜í”Œ ?ŒìФ ?˜ì •">
<option name="closed" value="true" />
<created>1783476059190</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1783476059191</updated>
</task>
<task id="LOCAL-00004" summary="?ŒìФ ?˜ì •">
<option name="closed" value="true" />
<created>1783563177105</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1783563177105</updated>
</task>
<task id="LOCAL-00005" summary="hr ?„로?<3F>트 ?? œ">
<option name="closed" value="true" />
<created>1783910068516</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1783910068516</updated>
</task>
<task id="LOCAL-00006" summary="hr ?„로?<3F>트 ?? œ">
<option name="closed" value="true" />
<created>1783910259418</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1783910259418</updated>
</task>
<task id="LOCAL-00007" summary="?´ëª¨ì§€ py ?Œì<C592>¼ ?? œ">
<option name="closed" value="true" />
<created>1783927746213</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1783927746213</updated>
</task>
<task id="LOCAL-00008" summary="?ŒìФ ?˜ì • ?„료">
<option name="closed" value="true" />
<created>1783931448779</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1783931448779</updated>
</task>
<task id="LOCAL-00009" summary="?ŒìФ ?˜ì • ?„료">
<option name="closed" value="true" />
<created>1783931684286</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1783931684286</updated>
</task>
<task id="LOCAL-00010" summary="?ŒìФ ?˜ì • ?„료">
<option name="closed" value="true" />
<created>1783931990807</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1783931990807</updated>
</task>
<task id="LOCAL-00011" summary="보통 금융ê¶?? í•œ?¼ì<C2BC>´?????€??ë³´í—˜?????´ë? ?ˆê±°???œìФ??>
<option name="closed" value="true" />
<created>1783992292399</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1783992292399</updated>
</task>
<task id="LOCAL-00012" summary="?ŒìФ ?˜ì •">
<option name="closed" value="true" />
<created>1783994134409</created>
<option name="number" value="00012" />
<option name="presentableId" value="LOCAL-00012" />
<option name="project" value="LOCAL" />
<updated>1783994134409</updated>
</task>
<task id="LOCAL-00013" summary="?ŒìФ ?˜ì •">
<option name="closed" value="true" />
<created>1783995434263</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1783995434263</updated>
</task>
<task id="LOCAL-00014" summary="?ŒìФ ?˜ì •">
<option name="closed" value="true" />
<created>1784001064429</created>
<option name="number" value="00014" />
<option name="presentableId" value="LOCAL-00014" />
<option name="project" value="LOCAL" />
<updated>1784001064429</updated>
</task>
<task id="LOCAL-00015" summary="?ŒìФ ?˜ì •">
<option name="closed" value="true" />
<created>1784094367009</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1784094367009</updated>
</task>
<task id="LOCAL-00016" summary="?˜ì • 버전">
<option name="closed" value="true" />
<created>1784098815515</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1784098815515</updated>
</task>
<task id="LOCAL-00017" summary="?˜ì • 버전">
<option name="closed" value="true" />
<created>1784105860688</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1784105860688</updated>
</task>
<task id="LOCAL-00018" summary="?˜ì • 버전">
<option name="closed" value="true" />
<created>1784161174384</created>
<option name="number" value="00018" />
<option name="presentableId" value="LOCAL-00018" />
<option name="project" value="LOCAL" />
<updated>1784161174384</updated>
</task>
<task id="LOCAL-00019" summary="?˜ì • 버전">
<option name="closed" value="true" />
<created>1784191131041</created>
<option name="number" value="00019" />
<option name="presentableId" value="LOCAL-00019" />
<option name="project" value="LOCAL" />
<updated>1784191131041</updated>
</task>
<task id="LOCAL-00020" summary="?˜ì • 버전111">
<option name="closed" value="true" />
<created>1784191828823</created>
<option name="number" value="00020" />
<option name="presentableId" value="LOCAL-00020" />
<option name="project" value="LOCAL" />
<updated>1784191828823</updated>
</task>
<task id="LOCAL-00021" summary="?˜ì • 버전111">
<option name="closed" value="true" />
<created>1784192641209</created>
<option name="number" value="00021" />
<option name="presentableId" value="LOCAL-00021" />
<option name="project" value="LOCAL" />
<updated>1784192641209</updated>
</task>
<task id="LOCAL-00022" summary="log 경로 ?˜ì •">
<option name="closed" value="true" />
<created>1784505706146</created>
<option name="number" value="00022" />
<option name="presentableId" value="LOCAL-00022" />
<option name="project" value="LOCAL" />
<updated>1784505706146</updated>
</task>
<option name="localTasksCounter" value="23" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="OPEN_GENERIC_TABS">
<map>
<entry key="2c2de5f9-e5f5-4565-9d3d-1bc8a056e08f" value="TOOL_WINDOW" />
<entry key="39879709-a44e-4eae-b130-7a588c5a5cd8" value="TOOL_WINDOW" />
<entry key="4b40888d-9986-4854-8984-53a06ca83373" value="TOOL_WINDOW" />
<entry key="7bdac3ab-6a4e-4e39-a92b-4a2c5ea226bd" value="TOOL_WINDOW" />
<entry key="26d7f06e-58b7-469d-946b-8ab4827b57da" value="TOOL_WINDOW" />
<entry key="efe781d4-6494-48db-a04e-65359ea237cd" value="TOOL_WINDOW" />
<entry key="860d5318-a419-41ba-a3c6-f009a6f61676" value="TOOL_WINDOW" />
<entry key="57842608-e2e0-4925-a066-a4c097b4b0ad" value="TOOL_WINDOW" />
</map>
</option>
<option name="TAB_STATES">
<map>
<entry key="26d7f06e-58b7-469d-946b-8ab4827b57da">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="HEAD" />
</list>
</value>
</entry>
<entry key="roots">
<value>
<list>
<option value="$PROJECT_DIR$" />
</list>
</value>
</entry>
</map>
</option>
<option name="SHOW_ONLY_AFFECTED_CHANGES" value="true" />
</State>
</value>
</entry>
<entry key="2c2de5f9-e5f5-4565-9d3d-1bc8a056e08f">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="HEAD" />
</list>
</value>
</entry>
<entry key="roots">
<value>
<list>
<option value="$PROJECT_DIR$" />
</list>
</value>
</entry>
</map>
</option>
<option name="SHOW_ONLY_AFFECTED_CHANGES" value="true" />
</State>
</value>
</entry>
<entry key="39879709-a44e-4eae-b130-7a588c5a5cd8">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="HEAD" />
</list>
</value>
</entry>
<entry key="roots">
<value>
<list>
<option value="$PROJECT_DIR$" />
</list>
</value>
</entry>
</map>
</option>
<option name="SHOW_ONLY_AFFECTED_CHANGES" value="true" />
</State>
</value>
</entry>
<entry key="4b40888d-9986-4854-8984-53a06ca83373">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="HEAD" />
</list>
</value>
</entry>
<entry key="roots">
<value>
<list>
<option value="$PROJECT_DIR$" />
</list>
</value>
</entry>
</map>
</option>
<option name="SHOW_ONLY_AFFECTED_CHANGES" value="true" />
</State>
</value>
</entry>
<entry key="57842608-e2e0-4925-a066-a4c097b4b0ad">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="HEAD" />
</list>
</value>
</entry>
<entry key="roots">
<value>
<list>
<option value="$PROJECT_DIR$" />
</list>
</value>
</entry>
</map>
</option>
<option name="SHOW_ONLY_AFFECTED_CHANGES" value="true" />
</State>
</value>
</entry>
<entry key="7bdac3ab-6a4e-4e39-a92b-4a2c5ea226bd">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="HEAD" />
</list>
</value>
</entry>
<entry key="roots">
<value>
<list>
<option value="$PROJECT_DIR$" />
</list>
</value>
</entry>
</map>
</option>
<option name="SHOW_ONLY_AFFECTED_CHANGES" value="true" />
</State>
</value>
</entry>
<entry key="860d5318-a419-41ba-a3c6-f009a6f61676">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="HEAD" />
</list>
</value>
</entry>
<entry key="roots">
<value>
<list>
<option value="$PROJECT_DIR$" />
</list>
</value>
</entry>
</map>
</option>
<option name="SHOW_ONLY_AFFECTED_CHANGES" value="true" />
</State>
</value>
</entry>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
<entry key="efe781d4-6494-48db-a04e-65359ea237cd">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="HEAD" />
</list>
</value>
</entry>
<entry key="roots">
<value>
<list>
<option value="$PROJECT_DIR$" />
</list>
</value>
</entry>
</map>
</option>
<option name="SHOW_ONLY_AFFECTED_CHANGES" value="true" />
</State>
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="index.html ?ŒìФ ?˜ì •" />
<MESSAGE value="?˜í”Œ ?ŒìФ ?˜ì •" />
<MESSAGE value="hr ?„로?<3F>트 ?? œ" />
<MESSAGE value="?´ëª¨ì§€ py ?Œì<C592>¼ ?? œ" />
<MESSAGE value="?ŒìФ ?˜ì • ?„료" />
<MESSAGE value="보통 금융ê¶?? í•œ?¼ì<C2BC>´?????€??ë³´í—˜?????´ë? ?ˆê±°???œìФ?? />
<MESSAGE value="?ŒìФ ?˜ì " />
<MESSAGE value="?˜ì • 버전" />
<MESSAGE value="?˜ì • 버전111" />
<MESSAGE value="log 경로 ?˜ì •" />
<option name="LAST_COMMIT_MESSAGE" value="log 경로 ?˜ì •" />
</component>
</project>

103
AddJavadoc.java Normal file
View File

@@ -0,0 +1,103 @@
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.regex.*;
public class AddJavadoc {
static final String ROOT_DIR = ".";
static final String TEMPLATE =
"/**\n" +
" * @package %s\n" +
" * @className %s\n" +
" * @description AX HUB ?쒖뒪??泥섎━ ?대옒??n" +
" * @author 源€?뺤떇\n" +
" * @create 2026.09.01\n" +
" * <pre>\n" +
" * ---------- 媛쒖젙?대젰 ----------\n" +
" * ?섏젙?? ?섏젙?? ?섏젙?댁슜\n" +
" * ---------- -------- ---------------------------\n" +
" * 2026.09.01 源€?뺤떇 理쒖큹?앹꽦\n" +
" * \n" +
" * </pre>\n" +
" */";
public static void main(String[] args) throws Exception {
Files.walkFileTree(Paths.get(ROOT_DIR), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
String name = dir.getFileName().toString();
if (name.equals(".git") || name.equals("build") || name.equals(".gradle") || name.equals("scratch") || name.equals("out") || name.equals("bin")) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.toString().endsWith(".java") && !file.getFileName().toString().equals("AddJavadoc.java")) {
processJavaFile(file);
}
return FileVisitResult.CONTINUE;
}
});
System.out.println("Done.");
}
private static void processJavaFile(Path file) throws IOException {
List<String> lines = Files.readAllLines(file);
String content = String.join("\n", lines);
if (content.contains("---------- 媛쒖젙?대젰 ----------") || content.contains("@className")) {
System.out.println("Skipping (already has javadoc): " + file);
return;
}
String packageName = "unknown";
String className = "unknown";
Matcher pkgMatcher = Pattern.compile("(?m)^\\s*package\\s+([\\w\\.]+)\\s*;").matcher(content);
if (pkgMatcher.find()) {
packageName = pkgMatcher.group(1);
}
int classDeclIdx = -1;
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
Matcher classMatcher = Pattern.compile("^\\s*(?:public\\s+|protected\\s+|private\\s+|abstract\\s+|final\\s+|static\\s+)*(class|interface|enum|record)\\s+(\\w+)").matcher(line);
if (classMatcher.find()) {
classDeclIdx = i;
className = classMatcher.group(2);
break;
}
}
if (classDeclIdx == -1) {
System.out.println("Skipping (no class declaration found): " + file);
return;
}
int insertIdx = classDeclIdx;
for (int i = classDeclIdx - 1; i >= 0; i--) {
String line = lines.get(i).trim();
if (line.isEmpty()) {
continue;
}
if (line.startsWith("@")) {
insertIdx = i;
} else if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) {
break;
} else {
break;
}
}
String javadoc = String.format(TEMPLATE, packageName, className);
lines.add(insertIdx, javadoc);
Files.write(file, String.join("\n", lines).getBytes("UTF-8"));
System.out.println("Updated: " + file);
}
}

View File

@@ -1,50 +1,31 @@
# 1. 빌드 환경 (JDK 21)
# -----------------------------------------------------------------------------
# 외부 인터넷이 차단된 내부망에서는 Docker Hub 대신 사내 Container Registry의
# Java 21 이미지를 사용합니다. 인프라 담당자에게 이미지의 전체 경로와 태그를
# 받은 후 아래 FROM 행만 교체합니다.
#
# 예: FROM registry.shinhanlife.co.kr/base/openjdk:21 AS builder
#
# 이 단계는 Gradle 빌드와 Java 컴파일을 수행하므로 반드시 JDK 21 이미지여야 합니다.
# 사내 이미지가 실제로 JDK 21인지 다음 명령으로 확인합니다.
# docker run --rm <사내-JDK-이미지> java -version
# 1. ??<3F><><EFBFBD>???<3F> (JDK 21)
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
# Gradle Wrapper와 소스 파일을 복사합니다.
# Gradle ??<3F>띁?? ???<3F><> 蹂듭<E8B982>?
COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
COPY src src
# Wrapper 실행 권한을 부여하고 테스트를 제외한 빌드를 수행합니다.
# 沅뚰<EFBFBD>??<3F>€??<3F>???<3F><><EFBFBD>?(???<3F><>????<3F>쇅)
RUN chmod +x gradlew
RUN ./gradlew clean build -x test
# 2. 실행 환경 (JRE 21)
# -----------------------------------------------------------------------------
# 실행 단계는 JRE 21 이미지가 가장 가볍지만, 사내에서 JDK 21 이미지만 제공하는
# 경우에는 동일한 JDK 21 이미지를 사용해도 정상 동작합니다.
#
# 예: FROM registry.shinhanlife.co.kr/base/openjre:21
# 예: FROM registry.shinhanlife.co.kr/base/openjdk:21
#
# 사내 제공 이미지의 기반 OS를 확인합니다.
# - Alpine 기반: 아래 apk 명령을 그대로 사용합니다.
# - Ubuntu/Debian 기반: apk 대신 apt-get update && apt-get install -y tzdata를 사용합니다.
# 2. ??<3F>뻾 ??<3F> (JRE 21)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# 서울 시간대를 설정합니다.
# ???꾩<>???<3F>젙 (??<3F>뎅 ??<3F>컙)
RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul
# 빌드 단계에서 생성한 애플리케이션 JAR를 복사합니다.
# ??<3F><><EFBFBD>??JAR ???<3F><> 蹂듭<E8B982>?
COPY --from=builder /app/build/libs/*.jar app.jar
# 애플리케이션 포트를 노출합니다.
# 湲곕???????몄텧
EXPOSE 8081
# 애플리케이션을 실행합니다.
ENTRYPOINT ["java", "-jar", "app.jar"]
# ?<3F>⑦???<3F><>???<3F>뻾 ??JAR ??<3F>
ENTRYPOINT ["java", "-jar", "app.jar"]

57
McpBridge.java Normal file
View File

@@ -0,0 +1,57 @@
/**
* @package io.shinhanlife
* @className McpBridge
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class McpBridge {
public static void main(String[] args) {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
try {
HttpRequest req = HttpRequest.newBuilder()
// Spring AI MCP Server??怨듭떇 ?⑥씪 ?붾뱶?ъ씤??
.uri(URI.create("http://localhost:8081/mcp"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(line))
.build();
HttpResponse<String> response = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
System.out.flush();
} catch (Exception e) {
// MCP ?쒖? ?먮윭 ?щ㎎?쇰줈 諛섑솚 (?꾩쓽濡?id 異붿텧 ?쒖쇅, 理쒖냼?쒖쓽 ?먮윭 ?묐떟)
System.out.println("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}}");
System.out.flush();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

842
README.md
View File

@@ -1,362 +1,606 @@
# AX HUB MCP Tool Platform
# AX HUB MCP Tool
## Current Tool Pod modules
신한라이프 업무 시스템과 AI Agent를 연결하는 MCP(Model Context Protocol) Gateway 및 Tool 서버 프로젝트입니다.
| Pod role | Gradle module | Container service | Internal endpoint | Local Docker endpoint |
|---|---|---|---|---|
| Customer/common integration | `dat-was-cus` | `was-cus` | `http://was-cus:8084/mcp` | `http://localhost:8284/mcp` |
| Sales/notification integration | `dat-was-sal` | `was-sal` | `http://was-sal:8082/mcp` | `http://localhost:8282/mcp` |
| Process integration (new) | `dat-was-pro` | `was-pro` | `http://was-pro:8085/mcp` | `http://localhost:8085/mcp` |
| System integration (new) | `dat-was-sys` | `was-sys` | `http://was-sys:8086/mcp` | `http://localhost:8086/mcp` |
Agent는 Gateway에서 Tool 목록과 입력 명세를 받고, Gateway는 권한과 정책을 확인한 뒤 Tool 서버로 요청을 전달합니다. 업무 Tool은 `DTO → UseCase Converter → MCI/EAI Client` 구조로 레거시 시스템을 호출합니다.
- The former `dat-was-oth` module is now `dat-was-cus`; the former `dat-was-sms` module is now `dat-was-sal`.
- This is a deployment Pod rename only. Existing tool category and function names such as `oth_*` and `sms_*` remain valid so already registered MCP clients are not broken.
- `dat-was-pro` and `dat-was-sys` are empty, independently deployable Tool Pods ready for new business tools.
### Run the Tool Pods locally
```powershell
.\gradlew.bat :dat-was-cus:bootRun
.\gradlew.bat :dat-was-sal:bootRun
.\gradlew.bat :dat-was-pro:bootRun
.\gradlew.bat :dat-was-sys:bootRun
```
AX HUB에서 AI Agent가 업무 Tool을 검색하고 호출할 수 있도록 Gateway와 독립 Tool Pod를 제공하는 멀티 모듈 Spring Boot 프로젝트입니다.
## 1. 현재 구성
## 전체 흐름
```text
MCP Client / AI Agent
|
v
Gateway (dat-gateway, 8081)
- MCP 연결, Tool Registry, 인증·인가, 라우팅, 공통 화면
|
+-------------------------+
| |
v v
Sales Tool Pod (dat-was-sal, 8082) Customer Tool Pod (dat-was-cus, 8084)
- 영업/알림 업무 Tool - 고객/공통 업무 Tool
- Tool Manifest - Tool Manifest
- Pod Test Console - Pod Test Console
|
+--> Process Tool Pod (dat-was-pro, 8085)
+--> System Tool Pod (dat-was-sys, 8086)
|
v
MCI / EAI / EIMS 등 레거시 연계 대상
AI Agent / MCP Client
MCP Gateway (dap-gateway)
├─ Tool 목록·스키마 제공
├─ Tool 권한·승인·가드레일 확인
├─ Redis Registry 및 실행 추적
└─ 대상 Tool 서버로 라우팅
Tool Server (dap-tool-sms / dap-tool-oth)
└─ BusinessToolController
UseCase → Converter → MCI/EAI Client → 레거시 시스템
```
Gateway는 외부 MCP 진입점과 Tool 등록·라우팅을 담당하고, 실제 업무 로직과 레거시 호출은 각 Tool Pod가 담당합니다. Tool Pod는 업무/조직 단위로 독립 배포할 수 있습니다.
## 현재 구조와 목표 구조
## 2. 모듈
현재는 Gateway와 두 개의 Tool 애플리케이션으로 구성됩니다.
| 모듈 | 역할 | 기본 포트 |
```text
현재: Gateway + SMS Tool Pod + OTH Tool Pod
목표: Gateway + 고객 Pod + 영업 Pod + 지급/납입 Pod + 알림 Pod + 인사 Pod + 공통 Pod
```
`dap-tool-oth`에는 여러 업무 카테고리가 함께 있습니다. AA 협의 후에는 부서·업무 소유권 단위로 Tool 서버, 이미지, Pod, 배포 파이프라인을 분리합니다. 이 목표 구조는 향후 전환 방향이며 현재 구현 완료 상태가 아닙니다.
## Gradle 멀티모듈
| 모듈 | 역할 | 실행 포트 |
|---|---|---:|
| `dat-gateway` | MCP Gateway, Registry, 라우팅, Chat·Catalog·Tester·Scaffold 화면 | 8081 |
| `dat-was-lib` | Tool 공통 라이브러리: 어노테이션, Schema, Manifest, MCI/EAI 어댑터, 공통 Web·보안 기능 | - |
| `dat-was-sal` | 영업/알림 업무 Tool Pod | 8082 |
| `dat-was-cus` | 고객/공통 업무 Tool Pod | 8084 |
| `dat-was-pro` | 프로세스 업무 Tool Pod | 8085 |
| `dat-was-sys` | 시스템 업무 Tool Pod | 8086 |
| `dap-gateway` | MCP 진입점, Tool Registry, 라우팅, 권한·가드레일, Chat API | 8081 |
| `dap-tool-core` | 공통 어노테이션, Controller, JSON Schema, MCI/EAI 지원, 보안·로깅 공통 기능 | 라이브러리 |
| `dap-tool-sms` | SMS/알림 Tool 서버 | 8082 |
| `dap-tool-oth` | 공통·업무·샘플·MCI Tool 서버 | 8084 |
> 기존 `dap-tool-core`, `dap-tool-sms`, `dap-tool-oth` 명칭은 현재 각각 `dat-was-lib`, `dat-was-sal`, `dat-was-cus`로 변경되었습니다.
기술 기준은 Java 21, Spring Boot 4, Gradle, Spring AI MCP Server, Redis, MapStruct, MyBatis, Resilience4j입니다.
## 3. 개발 환경
## 환경 (Environment)
| 항목 | 기준 |
| 항목 | 버전 / 기준 |
|---|---|
| Java | 21 |
| Spring Boot | 3.5.11 |
| Gradle | 8.14.3 (Wrapper) |
| Spring AI BOM | 1.1.8 |
| 기본 프로필 | `local` |
| Spring Boot | 4.0.5 |
| Gradle Wrapper | 8.14.3 |
| Spring AI BOM | 2.0.0 |
| Spring AI MCP Server | `spring-ai-starter-mcp-server-webmvc` |
| Redis Client | Lettuce 6.6.0.RELEASE |
| Resilience | Resilience4j 2.2.0 |
| MyBatis Spring Boot Starter | 3.0.3 |
| MapStruct | 1.5.5.Final |
| Lombok | 1.18.32 |
| JSON Schema Validator | networknt 1.4.0 |
| OpenAPI UI | springdoc 2.5.0 |
| 컨테이너 실행 | Docker Compose |
로컬 환경에서는 민감한 값은 환경 변수로만 주입합니다.
프로젝트는 JDK 21을 기준으로 컴파일됩니다. IntelliJ에서는 Project SDK, Gradle JVM, Run Configuration JRE를 모두 JDK 21로 맞춰야 합니다.
## 5분 빠른 시작
Docker와 JDK 21이 준비된 로컬 개발 환경 기준입니다.
```powershell
# 1. Redis와 MCI Mock만 먼저 실행
$env:ACTIVE_PROFILE = 'local'
docker compose up -d redis mci-mock
# 2. Gateway 실행 (새 PowerShell)
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:OPENROUTER_API_KEY = '<개발용 비밀 저장소의 키>'
.\gradlew.bat :dap-gateway:bootRun
# 3. OTH Tool 실행 (또 다른 PowerShell)
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:AXHUB_GATEWAY_URL = 'http://localhost:8081'
$env:AXHUB_TOOL_URL = 'http://localhost:8084'
.\gradlew.bat :dap-tool-oth:bootRun
```
Tool 서버가 기동된 뒤 아래 URL로 등록된 Tool 목록을 확인합니다.
```text
http://localhost:8081/mcp/api/v1/tools/list
```
SMS Tool도 함께 확인하려면 별도 PowerShell에서 아래 명령을 실행합니다.
```powershell
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:OPENROUTER_API_KEY = '<발급받은-키>'
$env:AXHUB_GATEWAY_URL = 'http://localhost:8081'
$env:AXHUB_TOOL_URL = 'http://localhost:8082'
.\gradlew.bat :dap-tool-sms:bootRun
```
## 4. 실행
### 4.1 Gradle 로컬 실행
각 애플리케이션은 별도 터미널에서 실행합니다.
```powershell
.\gradlew.bat :dat-gateway:bootRun
.\gradlew.bat :dat-was-sal:bootRun
.\gradlew.bat :dat-was-cus:bootRun
.\gradlew.bat :dat-was-pro:bootRun
.\gradlew.bat :dat-was-sys:bootRun
```
### 4.2 Docker Compose 실행
전체 컨테이너 환경이 필요하면 개별 실행 대신 다음 한 줄을 사용합니다.
```powershell
$env:ACTIVE_PROFILE = 'local'
docker compose up -d --build
```
## Tool 등록과 실행
| 서비스 | 호스트 URL | 컨테이너 포트 |
|---|---|---:|
| Gateway | http://localhost:8281 | 8081 |
| Sales Tool Pod | http://localhost:8282 | 8082 |
| Customer Tool Pod | http://localhost:8284 | 8084 |
| Process Tool Pod | http://localhost:8285 | 8085 |
| System Tool Pod | http://localhost:8286 | 8086 |
| MCI Mock | http://localhost:8089 | 8080 |
| Dozzle 로그 화면 | http://localhost:8288 | 8080 |
### 등록과 목록 제공
## 5. 화면과 운영 도구
1. Tool 서버 기동 시 `ToolRegistryHeartbeatSender``@McpTool`, `@McpFunction`을 스캔합니다.
2. Tool 이름, 설명, 입력 JSON Schema, `categoryKey`, 연동 방식, 실행 URL을 메타데이터로 생성합니다.
3. Gateway Redis Registry에 등록·Heartbeat 정보를 전송합니다.
4. Agent와 관리 화면은 Gateway에서 Tool 목록과 명세를 조회합니다.
### Gateway 화면
### 실행
| 화면 | 로컬 URL | 용도 |
1. Agent가 Gateway에 Tool 이름과 입력값을 보냅니다.
2. Gateway가 Tool 존재 여부, 허용 Tool, 쓰기 승인, 가드레일을 확인합니다.
3. Gateway가 Tool 서버의 `/mcp/{toolName}`으로 요청을 전달합니다.
4. `BusinessToolController`가 Tool 메서드를 찾아 DTO로 변환하고 JSON Schema를 검증합니다.
5. UseCase가 업무 흐름을 수행합니다.
6. Converter가 업무 DTO를 인터페이스 ID 기반 MCI 요청 DTO로 변환합니다.
7. MCI/EAI Client가 레거시를 호출하고 결과를 Tool 응답으로 반환합니다.
## 주요 URL
로컬에서 Gateway를 직접 실행할 때의 기준입니다. Docker Compose를 사용하면 Gateway 호스트 포트는 `8281`입니다.
| 용도 | 메서드 | URL |
|---|---|---|
| 메인 | http://localhost:8081/index.html | 플랫폼 진입 화면 |
| Catalog | http://localhost:8081/catalog.html | 등록 Tool 탐색 |
| Playground | http://localhost:8081/playground.html | 단건 Tool 호출 확인 |
| Chat | http://localhost:8081/chat.html | Agent 기반 대화형 호출 |
| Auto Tester | http://localhost:8081/tester.html | Gateway 기준 다중 Tool 스모크/회귀 테스트 |
| Scaffold | http://localhost:8081/admin/scaffold.html | Pod·Tool 소스 생성 지원 |
| Tool 목록 | `GET` | `http://localhost:8081/mcp/api/v1/tools/list` |
| Tool 실행 | `POST` | `http://localhost:8081/mcp/api/v1/tools/call` |
| Tool Markdown 문서 | `GET` | `http://localhost:8081/mcp/api/v1/tools/docs/markdown` |
| 카테고리별 SSE MCP 연결 | `GET` | `http://localhost:8081/mcp/sse/{categoryKey}` |
| 카테고리별 MCP 호출 채널 | `POST` | `http://localhost:8081/mcp/custom/{categoryKey}` |
| Chat 스트리밍 API | `POST` | `http://localhost:8081/api/chat` |
| Scaffold API | `POST` | `http://localhost:8081/api/v1/scaffold/pod` 또는 `/tool` |
Docker Compose로 실행한 경우 Gateway 화면은 포트 `8281`을 사용합니다.
`/mcp/sse/{categoryKey}`는 SSE 연결을 여는 전송 경로이고, `/mcp/custom/{categoryKey}`는 같은 카테고리의 MCP 요청을 처리하는 호출 경로입니다. 두 URL은 역할이 다릅니다.
### Pod Module New: 독립 프로젝트 생성
## Tool 개발 표준
Scaffold 화면의 `Pod Module New` 탭은 기존 Pod Module 생성 흐름과 별도로 동작합니다. 새 모듈명과 포트, workspace 경로를 입력하면 해당 workspace 바로 아래에 독립 Tool Pod 프로젝트를 생성합니다.
| 구성 요소 | 책임 |
|---|---|
| `XxxRequest`, `XxxResponse` | Agent/Tool 관점의 입력·응답 DTO |
| `XxxUseCase` | Tool 계약과 MCP 메타데이터 선언 |
| `XxxUseCaseImpl` | 업무 흐름 조합과 Client 호출 |
| `XxxConverter` | 업무 DTO와 레거시 인터페이스 DTO 사이 변환 |
| `MciXxxClient` | Glow/MCI 또는 EAI 통신 호출 |
| `INTERFACE_ID_I`, `INTERFACE_ID_O` | 인터페이스 ID 기준 MCI 요청·응답 DTO |
기본 workspace는 `C:\eGovFrameDev-4.3.1-64bit\workspace`이며, 예를 들어 모듈명으로 `dat-was-payment`를 입력하면 `C:\eGovFrameDev-4.3.1-64bit\workspace\dat-was-payment` 프로젝트가 만들어집니다. 모듈명은 `dat-was-`로 시작해야 하며, 이미 같은 폴더가 있으면 덮어쓰지 않고 생성 요청을 거절합니다.
### Tool 선언
생성된 프로젝트는 같은 workspace의 `dat-lib-datmt`를 Gradle composite build로 참조합니다. 생성 직후 프로젝트 폴더에서 다음 명령으로 컴파일할 수 있습니다.
Tool 그룹에는 `@McpTool`, Agent가 호출하는 함수에는 `@McpFunction`을 사용합니다.
```powershell
cd C:\eGovFrameDev-4.3.1-64bit\workspace\dat-was-payment
.\gradlew.bat compileJava
```
```java
@McpTool(routingType = "MCI", categoryKey = "claim")
public interface ClaimInquiryUseCase {
이 기능은 기존 AX HUB 멀티 모듈 Pod나 기존 `Pod Module` 탭의 구성·소스를 수정하지 않습니다.
### Tool Pod Test Console
공통 정적 화면인 `tool-test-console.html`은 Tool Pod의 `/tool-manifest`를 읽어, 해당 Pod에 등록된 Tool과 `inputSchema`를 기준으로 요청 JSON을 만들어 직접 실행합니다.
| 대상 Pod | 로컬 URL | Docker Compose URL |
|---|---|---|
| Customer | http://localhost:8084/tool-test-console.html | http://localhost:8284/tool-test-console.html |
| Sales | http://localhost:8082/tool-test-console.html | http://localhost:8282/tool-test-console.html |
| Process | http://localhost:8085/tool-test-console.html | http://localhost:8285/tool-test-console.html |
| System | http://localhost:8086/tool-test-console.html | http://localhost:8286/tool-test-console.html |
사용 방법은 다음과 같습니다.
1. 대상 Tool Pod의 Console에 접속합니다.
2. Tool을 선택하고 `Schema 샘플 채우기`로 요청 JSON을 생성합니다.
3. 업무에 맞는 값으로 보정한 뒤 실행합니다.
4. 재사용할 요청은 `현재 요청 저장`으로 브라우저 `localStorage`에 저장합니다.
### Gateway Auto Tester
`tester.html`은 Gateway에 등록된 Tool을 대상으로 다음 기능을 제공합니다.
- Tool 목록 자동 조회 및 Tool별 실행
- JSON Schema 기반 더미 요청 생성, 사용자 요청 JSON 저장
- 필터된 Tool 일괄 실행 및 실패 Tool 재실행
- 결과 차트, 실시간 실행 로그, CSV 내보내기, 호출 예제 복사
- Tool별 자동 Ping(3초 간격) 및 Stress Test(현재 50건 동시 요청)
> Auto Tester의 더미 요청 성공은 **통신·등록·기본 스키마 확인용 스모크 테스트**입니다. 실제 MCI/EAI 업무 규칙, 권한, 데이터 정합성은 보장하지 않습니다. Stress Test와 Auto Ping은 실제 Tool·레거시 호출을 유발할 수 있으므로 개발/테스트 환경 및 승인된 테스트 데이터에서만 사용합니다.
UI에서 사용하는 Tailwind CSS와 Chart.js는 `dat-gateway/src/main/resources/static/lib`에 포함되어 있어, 화면 라이브러리 로딩을 위해 외부 CDN 연결이 필요하지 않습니다.
## 6. 주요 HTTP API
### Gateway API
| 기능 | Method | 경로 |
|---|---|---|
| 등록 Tool 목록 | GET | `/mcp/api/v1/tools/list` |
| Gateway를 통한 Tool 호출 | POST | `/mcp/api/v1/tools/call` |
| Tool 문서 Markdown | GET | `/mcp/api/v1/tools/docs/markdown` |
| Tool 등록 | POST | `/mcp/api/v1/registry/register` |
| Tool 해제 | POST | `/mcp/api/v1/registry/deregister` |
| Heartbeat | POST | `/mcp/api/v1/registry/heartbeat` |
호출 본문은 JSON-RPC 형식을 사용합니다.
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "cmm_claim_search",
"arguments": {
"claimNo": "CLM2026070100120"
}
},
"id": 1
@McpFunction(
name = "claim_inquiry",
displayName = "보험금 청구 조회",
description = "청구 번호로 보험금 청구 상태를 조회합니다.",
mappingId = "CLM00000001"
)
ClaimInquiryResponse inquire(ClaimInquiryRequest request);
}
```
### Tool Pod API
`categoryKey`는 Tool의 업무 그룹입니다. Gateway의 목록 필터링, Agent 권한, 동적 MCP 서버 구분에 사용하므로 합의된 업무 키를 사용합니다.
| 기능 | Method | 경로 |
|---|---|---|
| 현재 Pod의 Tool Manifest | GET | `/tool-manifest` |
| 현재 Pod의 로컬 Tool 목록 | GET | `/mcp/api/v1/tools/local` |
| 현재 Pod에 직접 Tool 호출 | POST | `/mcp/{toolName}` |
- `MCI`: 사내 MCI 인터페이스 호출
- `EAI`: EAI 연동
- `DIRECT`: 외부 HTTP 또는 내부 직접 연동
Agent나 외부 클라이언트의 표준 MCP 진입은 Gateway를 사용합니다. Pod 직접 호출은 개발·단위 테스트·Pod Console 용도로 사용합니다.
### 변환 원칙
## 7. Tool 개발 규칙
UseCase 구현체는 Tool 요청을 레거시 요청과 섞어 쓰지 않습니다. Converter에서 변환한 뒤 Client에 전달합니다.
### 이름
```java
@Override
public ClaimInquiryResponse inquire(ClaimInquiryRequest request) {
CLM00000001_I mciRequest = converter.toMciRequest(request);
CLM00000001_O mciResponse = mciClmClient.callClm00000001(mciRequest);
return converter.toResponse(mciResponse);
}
```
Tool 함수명은 아래 4단계 규칙을 사용합니다.
MCI 입출력 객체는 업무 이름이 아니라 인터페이스 ID를 기준으로 둡니다.
```text
pod_domain_service_action
예: cmm_claim_search
CLCNNB00001_I : CLCNNB00001 요청 DTO
CLCNNB00001_O : CLCNNB00001 응답 DTO
```
- `pod`: Tool Pod 식별자 (`oth`, `sms` 등)
- `domain`: 업무 도메인 또는 `categoryKey` (`cmm`, `smp` 등)
- `service`: 업무 서비스명
- `action`: 동작 (`search`, `issue`, `send` 등)
예를 들어 `Onnba3011Request``CLCNNB00001_I`는 같은 업무 데이터를 담을 수 있지만 같은 객체가 아닙니다. 둘 사이의 변환 책임은 `Onnba3011Converter`에 둡니다.
### 기본 구현 계층
### 새 Tool 추가 체크리스트
```text
Request DTO
-> UseCase Interface
-> UseCaseImpl
-> Converter (AI DTO <-> MCI/EAI Interface IO)
-> Client / Adapter
-> Legacy System
```
새 업무 Tool을 추가할 때는 아래 순서로 확인합니다.
- Request DTO 필드에는 `@Schema(description = "...")`와 Bean Validation 어노테이션을 선언합니다.
- 복잡하거나 조건부 규칙이 필요한 입력은 `tool-schemas/{categoryKey}/...-input-schema.json` 리소스를 사용합니다.
- 응답 Schema가 필요한 경우 Response DTO에 `@McpOutputSchema` 선언하고, 복잡한 경우 Output Schema 리소스로 명시합니다.
- `null`의 업무 의미, 조건부 필드, 배열 정렬 기준, `hasMore` 여부, 민감 정보 제외 원칙을 Schema 설명에 명확히 씁니다.
- [ ] 소속 모듈과 `categoryKey`를 업무 소유 조직 기준으로 결정한다.
- [ ] Agent 입력·응답 DTO인 `XxxRequest`, `XxxResponse`를 만든다.
- [ ] `XxxUseCase``@McpTool` 선언하고, 호출 메서드에 `@McpFunction`의 이름·설명·연동 ID를 선언한다.
- [ ] `XxxUseCaseImpl`에서 업무 흐름만 조합한다.
- [ ] `XxxConverter`에 업무 DTO ↔ 인터페이스 ID DTO 변환을 둔다.
- [ ] `MciXxxClient``INTERFACE_ID_I`, `INTERFACE_ID_O`를 인터페이스 ID 기준으로 만든다.
- [ ] DTO에 Bean Validation을 선언하고, 중첩 DTO가 있으면 입력 Schema와 검증 대상에 포함되는지 확인한다.
- [ ] 조회·변경 작업 특성에 따라 `readOnlyHint`, `requiresApproval`, `idempotentHint`를 설정한다.
- [ ] 단위 테스트를 작성하고 `:dap-tool-core:test` 또는 대상 모듈 테스트를 실행한다.
- [ ] Tool 서버 기동 후 `/mcp/api/v1/tools/list`에서 이름, 설명, category, JSON Schema가 맞는지 확인한다.
- [ ] 요청·응답 로그에 개인정보나 인증값이 남지 않는지 확인한다.
### 등록·노출 제어
### Schema 우선순위
1. `inputSchemaResource` 또는 `outputSchemaResource`가 지정된 경우: 해당 JSON Schema를 사용합니다.
2. 리소스가 없으면: DTO의 `@Schema` 정보와 공통 Generator로 생성합니다. 응답 Schema는 `@McpOutputSchema`가 선언된 DTO만 자동 생성합니다.
## 8. Manifest와 Registry
각 Tool Pod는 기동 시 `@McpTool`, `@McpFunction` 정보를 읽어 `/tool-manifest`를 제공합니다. Manifest에는 Tool 이름, 설명, endpoint, revision, Input/Output Schema가 포함됩니다.
Gateway는 Registry 등록 및 Heartbeat 정보를 이용해 Tool을 라우팅합니다. Tool Pod가 추가되어도 동일한 Manifest/Registry 규약을 준수하면 Gateway에서 탐색·호출할 수 있습니다.
## 9. MCI/EAI 연계
공통 MCI/EAI 연계 기능과 Glow 기본 설정은 `dat-was-lib`에서 제공합니다. Tool Pod별 애플리케이션 프로필은 각 Pod의 `application*.yml`에서 관리합니다.
```text
dat-was-lib/src/main/resources/glow/application-glow*.yml
dat-was-sal/src/main/resources/application*.yml
dat-was-cus/src/main/resources/application*.yml
dat-was-pro/src/main/resources/application*.yml
dat-was-sys/src/main/resources/application*.yml
```
MCI 호출 주소는 일반적으로 `host + uri`로 구성합니다. 예를 들어 `host=https://dev-ichmci.shinhanlife.co.kr`, `uri=/ntl_mci/clc_rcv`이면 호출 대상은 다음과 같습니다.
```text
https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv
```
`receive-uri`는 Tool이 호출할 주소가 아니라, MCI가 비동기/콜백 방식으로 응답을 전달하도록 별도 계약된 경우 수신에 사용하는 경로입니다.
## 10. 품질 검증
```powershell
.\gradlew.bat :dat-was-lib:test
.\gradlew.bat :dat-was-cus:compileJava
.\gradlew.bat :dat-was-sal:compileJava
.\gradlew.bat :dat-was-pro:compileJava
.\gradlew.bat :dat-was-sys:compileJava
.\gradlew.bat validateMcpToolNames
```
배포 전에는 다음을 확인합니다.
- Tool 이름의 전역 중복 여부와 `pod_domain_service_action` 규칙(아래 확인 사항 반영 후)
- Request/Response Schema 및 실제 예제 JSON
- Tool Pod 단위 테스트와 Gateway 경유 호출
- MCI/EAI 오류 코드의 사용자용 응답 매핑
- 민감정보가 요청·응답·로그에 포함되지 않는지
- 승인된 개발/테스트 계정과 데이터만 사용했는지
## 11. 보안과 운영 원칙
- API Key, Runner 등록 토큰, 계정·비밀번호 등 비밀값은 Git에 넣지 않고 환경 변수 또는 Secret Manager로 주입합니다.
- `TESTER-DEV` 같은 테스트 권한은 개발 환경에서만 허용하고, 운영에서는 Tool 단위 최소 권한으로 제한합니다.
- Tool 요청·응답 로그는 마스킹/제외 정책을 거친 값만 남깁니다.
- Trace ID는 요청 흐름 전체를 추적하고, Request ID는 HTTP 호출 단위로 새로 부여합니다.
- Tool Pod는 업무 소유권과 장애 격리 단위에 맞춰 독립 이미지·배포·운영 책임으로 분리합니다.
## 12. 참고 소스 위치
| 기능 | 위치 |
| 속성 | 의미 |
|---|---|
| Tool 공통 Controller | `dat-was-lib/src/main/java/io/shinhanlife/dat/mcc/presentation` |
| Tool 어노테이션 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/annotation` |
| JSON Schema/검증 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/schema`, `.../validation` |
| Manifest | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest` |
| Tool Scaffold | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/ToolScaffolder.java` |
| Gateway 라우터 | `dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/McpRouterController.java` |
| Customer Tool | `dat-was-cus/src/main/java` |
| Sales Tool | `dat-was-sal/src/main/java` |
| Process Tool | `dat-was-pro/src/main/java` |
| System Tool | `dat-was-sys/src/main/java` |
## 13. 모듈명 전환 검증
| `register` | Redis Registry와 Gateway 카탈로그에 등록할지 여부 |
| `visible` | Agent/클라이언트 목록에 표시할지 여부 |
| `requiresApproval` | 쓰기·고위험 작업의 승인 요구 여부 |
| `readOnlyHint`, `destructiveHint`, `idempotentHint` | Agent 호출 특성 힌트 |
Tool 관련 공통 기능은 `dat-was-*` 모듈명만 기준으로 동작합니다.
`register = false`인 Tool은 자동 카탈로그 등록 대상이 아닙니다. 별도 실행 목적이 있는 경우에만 사용하고, 필요한 fallback 경로를 운영 설정으로 확인합니다.
- `validateMcpToolNames``dat-was-*` Tool Pod를 탐색하여 이름 규칙과 전역 중복을 검사합니다.
- Tool Scaffold는 Pod 이름을 Tool 함수명에 포함하지 않습니다. 함수명은 `도메인_비즈니스_행위` 형식입니다. 예: `cmm_notification_send`
- Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dat-was-*` 명칭으로 통일되어 있습니다.
- Tool Source Update 기능은 `dat-was-*` 아래의 `*UseCase.java`를 검색합니다.
## 빌드·테스트·로컬 실행
## 14. BC-DAB-STD-003 Tool Schema V17 적용
### 사전 조건
각 Tool의 표준 명세는 Tool Pod별 다음 경로에서 관리합니다.
- JDK 21
- Gradle Wrapper 사용 권장
- 로컬 Redis 또는 Docker Compose 환경
- 필요 시 MCI Mock 또는 사내 MCI/EAI 접근 환경
```text
dat-was-{pod}/src/main/resources/tool-definitions/{categoryKey}/{toolName}.yml
```
Tool 이름은 Pod 정보를 포함하지 않는 `도메인_서비스_행위` 형태의 영문 소문자 snake_case를 사용하며,
정규식 `^[a-z][a-z0-9_]{2,63}$`을 만족해야 합니다. 예: `cmm_claim_search`.
필수 항목은 `name`, `display_name`, `version`, `category_key`, 설명 4개 요소(function, when_to_use,
when_not_to_use, io_limits), `display_description`, 예시 질의 3~10건, 동작 힌트 3개(read_only,
destructive, idempotent), `parameters_schema`입니다. 입력 Schema는 루트 `type: object`, 각 property의
`description`, `additionalProperties: false`를 갖춰야 합니다. 선택 운영 항목은 `tags`,
`legacy_interface_id`, `required_env_keys`, `owner_org`입니다.
기동 시 `tool-definitions/**/*.yml`을 한 번 읽어 이름 기준으로 캐시하고, `@McpTool` 실행 정보와 결합한
동일한 `ToolMetadata``/tool-manifest`, Tool Pod MCP, Gateway MCP에 사용합니다.
입력 Schema 우선순위는 `inputSchemaResource` → V17 `parameters_schema` → DTO 자동 생성이고, 출력은
`outputSchemaResource` → 명시 Output Schema → `@McpOutputSchema` 기반 생성입니다. Output Schema를
명시한 Tool만 최종 응답 검증을 수행합니다. 현재 Scaffold는 Tool Definition V17 YAML과 응답 Mock JSON을
자동 생성하지 않고, 생성한 Java Tool의 `@GrowToolHint`를 기준으로 메타데이터를 제공합니다.
Scaffold의 Pod Module 생성은 `local`, `dev`, `test`, `prod` 프로필 파일과
`src/main/resources/tool-service-manifest.yml`을 만듭니다. 이 파일은 라우팅용 설정이며 `/tool-manifest`
API 응답을 대체하지 않습니다. AI 초안은 한국어 YAML로 생성하며, 선택한 Target Module 목록에서 자기
자신을 제외해 `confusable-servers`에 반영합니다. Tool Function의 기본 Protocol은 MCI(Legacy)입니다.
### 전체 빌드와 대표 검증
```powershell
.\gradlew.bat validateMcpToolNames validateToolSchemaV17
.\gradlew.bat clean build
.\gradlew.bat :dap-tool-core:test
.\gradlew.bat :dap-tool-core:compileJava
```
`bootJar`는 두 검증에 의존하므로 이름 중복, 필수 항목 누락, Java Tool과 YAML 명세 불일치가 있으면
Docker 이미지 생성 전에 빌드가 실패합니다.
### 애플리케이션 실행
각 애플리케이션은 별도 PowerShell에서 실행합니다.
```powershell
# Gateway
.\gradlew.bat :dap-gateway:bootRun
# SMS Tool
.\gradlew.bat :dap-tool-sms:bootRun
# 기타 업무 Tool
.\gradlew.bat :dap-tool-oth:bootRun
```
기본 프로필은 `local`입니다. 개발 서버 설정이 필요하면 실행 환경에 프로필을 지정합니다.
```powershell
$env:SPRING_PROFILES_ACTIVE = 'dev'
.\gradlew.bat :dap-tool-oth:bootRun
```
## Docker Compose 실행
Docker Compose는 Redis, Gateway, MCI Mock, SMS Tool, OTH Tool을 함께 기동합니다.
```powershell
$env:ACTIVE_PROFILE = 'local'
docker compose up -d --build
docker compose ps
```
| 서비스 | 컨테이너 포트 | 호스트 포트 |
|---|---:|---:|
| Redis | 6379 | 6379 |
| Gateway | 8081 | 8281 |
| MCI Mock | 8080 | 8089 |
| SMS Tool | 8082 | 8282 |
| OTH Tool | 8084 | 8284 |
컨테이너 내부와 PC 브라우저의 접속 주소는 다릅니다.
```text
컨테이너 내부: http://gateway:8081, http://tool-oth:8084
PC 브라우저: http://localhost:8281, http://localhost:8284
```
## 환경 설정
### 프로필
기본 프로필은 `local`입니다. Tool 서버와 Gateway 모두 `local`, `dev` 프로필 파일을 사용합니다.
| 프로필 | 목적 | 주요 설정 파일 |
|---|---|---|
| `local` | PC 개발·MCI Mock·H2 메모리 DB 기반 실행 | `application-local.yml` |
| `dev` | 개발 서버 배포 실행 | `application-dev.yml` |
프로필은 환경 변수로 지정합니다.
```powershell
# 로컬 개발
$env:SPRING_PROFILES_ACTIVE = 'local'
# 개발 서버 설정으로 실행
$env:SPRING_PROFILES_ACTIVE = 'dev'
```
### 환경별 연결 주소 비교
| 항목 | `local` 프로세스 실행 | Docker Compose | `dev` 프로필 |
|---|---|---|---|
| Gateway 접근 주소 | `http://localhost:8081` | 컨테이너 내부 `http://gateway:8081` | 배포 환경 Gateway URL |
| SMS Tool 주소 | `http://localhost:8082` | `http://tool-sms:8082` | `PORT` 기본 8082 |
| OTH Tool 주소 | `http://localhost:8084` | `http://tool-oth:8084` | `PORT` 기본 8084 |
| Redis 주소 | 로컬 Redis 또는 `localhost:6379` | `redis:6379` | 운영/개발 Redis 설정 |
| MCI/EAI 대상 | MCI Mock 또는 로컬 설정 | `mci-mock:8080` | 개발망 연동 설정 |
| 브라우저 Gateway 접속 | `http://localhost:8081` | `http://localhost:8281` | 운영·개발 도메인 |
`local`에서 프로세스를 직접 실행하면 Tool의 `AXHUB_GATEWAY_URL``localhost`를 사용합니다. Docker에서는 각 컨테이너가 서로 다른 네트워크 공간에 있으므로 반드시 Compose 서비스 이름을 사용합니다.
### Gateway 환경 변수
| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 |
|---|---|---|---|
| `SPRING_PROFILES_ACTIVE` | Gateway, 모든 Tool | 활성 Spring 프로필 | `local` |
| `OPENROUTER_API_KEY` | Gateway | Chat/LLM 호출 API 키 | 운영·개발 환경의 비밀 저장소에서 주입 |
| `SPRING_DATA_REDIS_HOST` | Gateway, 모든 Tool | Redis 호스트 | Docker: `redis` |
| `SPRING_DATA_REDIS_PORT` | Gateway, 모든 Tool | Redis 포트 | `6379` |
| `MCP_GATEWAY_FALLBACK_DEFAULT_URL` | Gateway | Registry에 없는 Tool의 기본 fallback URL | Docker: `http://tool-oth:8084` |
| `MCP_GATEWAY_FALLBACK_ROUTES_SMS` | Gateway | SMS 계열 fallback URL | Docker: `http://tool-sms:8082` |
| `ACTIVE_PROFILE` | Docker Compose | Compose가 `SPRING_PROFILES_ACTIVE`에 전달할 프로필 | `local` |
`OPENROUTER_API_KEY` 같은 인증값은 `application.yml`, README, Git 커밋에 직접 넣지 않습니다. 개발·운영 환경의 Secret, CI/CD 변수 또는 안전한 환경 변수로 주입합니다.
### Tool 서버 환경 변수
| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 |
|---|---|---|---|
| `PORT` | `dap-tool-sms`, `dap-tool-oth` | 개발 프로필에서 Tool 서버 포트 변경 | SMS `8082`, OTH `8084` |
| `AXHUB_GATEWAY_URL` | 모든 Tool | Tool 등록·Heartbeat 대상 Gateway 주소 | 로컬 `http://localhost:8081`, Docker `http://gateway:8081` |
| `AXHUB_TOOL_URL` | 모든 Tool | Gateway가 해당 Tool Pod를 호출할 주소 | 로컬 `http://localhost:{server.port}` |
| `GLOW_COMMUNICATION_MCI_HOMT` | Docker Tool 컨테이너 | MCI 대상 호스트 | 로컬 Compose는 `mci-mock` |
| `GLOW_COMMUNICATION_MCI_PORT` | Docker Tool 컨테이너 | MCI 대상 포트 | 로컬 Compose는 `8080` |
| `GLOW_COMMUNICATION_EAI_HOMT` | Docker Tool 컨테이너 | EAI 대상 호스트 | 로컬 Compose는 `mci-mock` |
| `GLOW_COMMUNICATION_EAI_PORT` | Docker Tool 컨테이너 | EAI 대상 포트 | 로컬 Compose는 `8080` |
`AXHUB_TOOL_URL`은 반드시 Gateway가 실제로 접근 가능한 주소여야 합니다. PC에서 각각 실행할 때는 `localhost`를 사용하고, Docker 컨테이너 안에서는 `tool-sms`, `tool-oth` 같은 Compose 서비스 이름을 사용합니다.
### 로컬 실행용 권장 설정
아래는 키 값 없이 로컬 프로세스를 실행하는 예시입니다. Redis를 Docker로 먼저 기동하거나 전체 Docker Compose를 사용합니다.
```powershell
# 선택 1: Redis만 기동
$env:ACTIVE_PROFILE = 'local'
docker compose up -d redis mci-mock
# 선택 2: 각 프로세스를 로컬에서 기동
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:OPENROUTER_API_KEY = '<개인 또는 개발용 비밀 저장소의 키>'
.\gradlew.bat :dap-gateway:bootRun
```
다른 PowerShell에서 Tool을 실행합니다.
```powershell
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:AXHUB_GATEWAY_URL = 'http://localhost:8081'
$env:AXHUB_TOOL_URL = 'http://localhost:8084'
.\gradlew.bat :dap-tool-oth:bootRun
```
### 권한 도메인 설정
`mcp.security.tenant-domains`는 테넌트 또는 호출 주체가 접근할 수 있는 `categoryKey`를 정의합니다. 운영 환경에서는 `ALL`을 무분별하게 사용하지 않고, Agent·조직별 허용 도메인을 최소 권한으로 설정합니다.
```yaml
mcp:
security:
tenant-domains:
claims-agent: claim, customer
notification-agent: notification
```
## 공통 기능과 운영 기준
### Redis
Redis는 Tool Registry의 등록 상태와 Heartbeat, Gateway 실행 추적 정보에 사용됩니다. 현재 Redis는 PII 원문 보관소가 아닙니다.
### 권한과 승인
Gateway는 Agent가 전달한 허용 Tool 목록, 서버 정책, 신뢰된 Claim 여부, 쓰기 승인 여부를 확인합니다. Tool마다 권한 로직을 중복 구현하지 말고 Gateway 공통 정책과 `@McpFunction` 메타데이터를 사용합니다.
### Trace ID와 Request ID
```text
trace-id : 사용자 요청 전체에서 유지되는 상관관계 ID
request-id : Gateway → Tool, Tool → MCI 등 HTTP 호출마다 새로 생성되는 ID
```
Gateway와 Tool에는 관련 헤더 및 MDC 기반 로그 처리가 있습니다. 신규 HTTP Client도 공통 전파 정책을 따르며, Tool별로 임의의 헤더 이름을 추가하지 않습니다.
### 로그와 개인정보
Gateway에는 민감 키와 일부 형식을 마스킹하는 공통 기능이 있습니다. 마스킹은 원문을 Agent에서 분리하는 PII 토큰화와는 다릅니다.
- 요청·응답 전문을 로그에 남길 때는 반드시 마스킹합니다.
- 운영 로그에 주민번호, 계좌번호, 전화번호, 이메일, 인증값을 남기지 않습니다.
- Tool 서버의 신규 로그도 같은 마스킹 정책을 적용합니다.
- 민감정보 복원 필요 여부는 Tool 개발자가 임의로 결정하지 않고 보안·AA 정책을 따릅니다.
## AA 협의 기반 향후 전환 과제
아래 항목은 현재 구현 완료 기능이 아니라 회의에서 합의한 목표 구조입니다.
### 부서별 Tool Pod와 저장소 경계
현재 `dap-tool-oth`에 함께 있는 업무 Tool을 부서·업무 소유권 단위로 분리합니다. 각 Pod는 독립 이미지, 독립 배포, 독립 장애 범위를 갖도록 구성합니다. 실제 분리는 AA가 확정한 Tool 소유 부서와 운영 책임 매핑을 기준으로 수행합니다.
### Agent별 Tool 노출 수 제한
Agent에게 모든 Tool을 한 번에 제공하지 않고, 업무 도메인과 권한에 따라 약 10~20개 수준의 Tool 그룹을 제공합니다. `categoryKey`는 이를 위한 기초 메타데이터이며, 향후 Agent-Tool Group 정책으로 확장합니다.
### PII 토큰화
```text
원문 개인정보
→ PII Gateway가 Redis에 짧은 TTL로 보관
→ Agent에는 PII 토큰 또는 안전한 식별자만 전달
→ 인가된 Tool이 MCI 호출 직전에 필요한 항목만 복원
```
이 전환 전까지는 현행 마스킹 기능을 PII 분리 구현으로 오해하지 않아야 합니다.
### MCP SDK와 Glow Framework
Gateway에는 Spring AI MCP Server 의존성이 포함되어 있습니다. MCP SDK/Glow 표준 적용 시에는 업무 Tool을 재작성하지 않고, 기존 `@McpTool`·`@McpFunction`과 UseCase를 표준 MCP Tool 명세·호출 콜백으로 연결하는 Adapter 계층을 공통 Core에 추가합니다.
```text
MCP SDK 표준 tools/list, tools/call
공통 Adapter
기존 UseCase → Converter → MCI Client
```
따라서 업무 DTO, Converter, MCI Client의 책임은 유지됩니다.
## 참고 소스
| 주제 | 대표 위치 |
|---|---|
| Gateway Tool API | `dap-gateway/.../presentation/McpRouterController.java` |
| 동적 MCP SSE/호출 경로 | `dap-gateway/.../sync/DynamicMcpController.java` |
| Tool 실행 Controller | `dap-tool-core/.../presentation/BusinessToolController.java` |
| Tool 자동 등록 | `dap-tool-core/.../usecase/ToolRegistryHeartbeatSender.java` |
| Tool 어노테이션 | `dap-tool-core/.../annotation/McpTool.java`, `McpFunction.java` |
| Tool 예시 | `dap-tool-oth/.../biz/oth`, `biz/sol`, `biz/smp` |
| SMS Tool 예시 | `dap-tool-sms/.../biz/sms` |
| Docker 환경 | `docker-compose.yml` |
---
문서에 없는 업무·보안·배포 기준은 임의로 추가하지 말고 AA 및 플랫폼 운영 기준과 먼저 합의합니다.
## Input/Output Schema 작성 가이드
Tool Schema는 Agent가 Tool을 정확히 호출하고, 반환값의 의미를 일관되게 해석하도록 하는 계약입니다. 인증 정보·사번·주민번호 등 민감정보(PII)는 Input/Output Schema와 Tool 응답에 포함하지 않습니다.
### Input Schema
Input Schema는 Agent가 Tool에 전달하는 파라미터의 이름, 타입, 필수 여부, 허용값, 형식 등을 정의합니다.
적용 우선순위는 다음과 같습니다.
1. `inputSchemaResource` — 복잡한 규칙을 담은 JSON Schema 리소스
2. `inputSchema` — 어노테이션에 직접 선언한 JSON Schema
3. 요청 DTO 필드의 `@McpValidation` — 자동 JSON Schema 생성
단순한 요청 DTO는 `@McpValidation`만으로 관리합니다.
```java
public class ClaimSearchRequest {
@McpValidation(required = true, pattern = "^CLM[0-9]{13}$")
private String claimNo;
@McpValidation(minimum = 1, maximum = 100)
private Integer size;
}
```
### Output Schema
Output Schema는 Tool이 반환하는 결과의 타입과 의미를 정의합니다. `BusinessToolController`는 Tool 실행 후 반환값을 Output Schema 기준으로 검증합니다.
적용 우선순위는 다음과 같습니다.
1. `outputSchemaResource` — 조건부 필드·중첩 배열 등 복잡한 규칙을 담은 JSON Schema 리소스
2. `outputSchema` — 어노테이션에 직접 선언한 JSON Schema
3. 반환 DTO의 `@McpOutputSchema`와 필드 `@McpValidation` — 자동 JSON Schema 생성
4. 위 설정이 모두 없으면 Output Schema 검증을 수행하지 않음
따라서 단순한 응답은 별도 `outputSchemaResource` 없이 반환 DTO에 `@McpOutputSchema`를 선언하면 됩니다. `null`이 정상 값일 수 있는 필드는 `nullable = true`를 반드시 지정합니다.
```java
@McpOutputSchema
public class ClaimSearchResponse {
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
private String resultCode;
@McpValidation(nullable = true, minimum = 0)
private Long approvedAmount;
}
```
### 복잡한 Schema는 Tool 모듈별 리소스로 관리
조건부 응답, 중첩 DTO, 배열 정렬 기준처럼 어노테이션만으로 표현하기 어려운 규칙은 Tool Core가 아니라 각 Tool 모듈의 리소스에 JSON Schema로 둡니다.
```text
src/main/resources/
└─ tool-schemas/
└─ {categoryKey}/
├─ claim-search-resource-input-schema.json
└─ claim-search-resource-output-schema.json
```
예를 들어 `categoryKey``cmm`이면 아래와 같이 선언합니다.
```java
@McpFunction(
name = "oth.cmm.claim.search",
inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json"
)
public ClaimSearchResponse search(ClaimSearchRequest request) {
// ...
}
```
`inputSchemaResource``outputSchemaResource`는 복잡한 경우에만 선언합니다. 단순한 Tool까지 JSON 파일을 별도 생성할 필요는 없습니다.
### Output 설계 규칙
- 코드와 표시용 라벨을 함께 반환합니다. 예: `status` + `statusLabel`
- `null`이 정상인 값은 의미를 설명에 명시하고 DTO에는 `nullable = true`를 설정합니다.
- 조건부 필드는 어떤 조건에서 값이 존재하는지 JSON Schema에 명시합니다.
- 배열은 정렬 기준을 설명에 명시합니다. 예: `접수일 내림차순`
- 목록 응답에는 추가 조회 여부를 나타내는 `hasMore`를 포함합니다.
- 민감정보는 마스킹보다 **응답에서 제외**하는 것을 우선합니다.
### 실행 로그 및 확인
Tool 실행이 끝나면 아래 로그는 Schema 정의가 아니라 **검증을 통과한 실제 최종 응답값**을 출력합니다.
```text
[Tool -> MCP Gateway] Output Schema Result: { ... }
```
따라서 로그에도 실제 응답이 남으므로, 응답 DTO와 Output Schema에 민감정보가 포함되지 않도록 설계해야 합니다.
스키마 리소스와 DTO 기반 자동 Schema는 아래 테스트로 함께 검증할 수 있습니다.
```powershell
.\gradlew.bat :dap-tool-oth:test --tests "io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequestSchemaTest"
```
### Tool Naming Convention
All Tool names use the four-level lowercase format `pod.domain.service.action`. Do not use underscores or CamelCase; use a hyphen (`-`) only when a single level has multiple words.
- `pod`: deployment Tool Pod/module (`dap-tool-oth``oth`, `dap-tool-sms``sms`)
- `domain`: business-domain package (`cmm`, `smp`, `sol`, etc.)
- `service`: business service or resource
- `action`: the requested operation (`search`, `list`, `detail`, `issue`, `inquiry`, etc.)
```text
oth.cmm.bond.issue
oth.cmm.claim.search
oth.sol.request.list
oth.smp.weather.inquiry
```
When Scaffold receives `dap-tool-oth`, `cmm`, and `ClaimSearch`, it generates `oth.cmm.claim.search`. The `validateMcpToolNames` Gradle task rejects both a duplicate name and any name outside this format before packaging, including its source file and line number.
### Tool Test Console
각 Tool Pod는 공통 테스트 화면을 제공합니다.
```text
http://localhost:8084/tool-test-console.html
```
화면은 현재 Pod의 `/tool-manifest`에서 Tool 목록과 `inputSchema`를 읽습니다. Tool을 선택한 뒤 `Schema 샘플 채우기`로 요청 JSON을 만들고 실행할 수 있습니다. 업무에 맞게 보정한 요청은 `현재 요청 저장`으로 브라우저의 `localStorage`에 보관합니다.
`Run saved cases`는 저장된 테스트 케이스를 순차 실행해 성공/실패, HTTP 상태, 소요 시간을 보여줍니다. 따라서 Tool이 수백 개여도 각 Tool마다 테스트 화면을 만들 필요 없이, 유효한 업무 테스트 데이터만 한 번 저장하면 이후에는 몇 번의 클릭으로 회귀 테스트할 수 있습니다.
- Tool 호출은 현재 Pod의 `/mcp/{toolName}`로 수행합니다.
- 매 실행마다 `trace-id`, `request-id`를 새로 생성하여 응답과 함께 표시합니다.
- 외부 MCI/EAI Tool은 샘플값 대신 개발계에서 허용된 테스트 데이터를 저장해서 사용해야 합니다.

View File

@@ -1,7 +0,0 @@
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
public class TestClientTransport {
public static void main(String[] args) {
HttpClientStreamableHttpTransport t = HttpClientStreamableHttpTransport.builder("http://localhost:8086/mcp").build();
System.out.println("Wait, I can't print private fields easily, but let's just see if it runs.");
}
}

View File

@@ -1,22 +0,0 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class TestMcpCall {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String json = "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"iam_team_contact\",\"arguments\":{\"target\":\"전체\"}},\"id\":1}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8086/mcp"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer dapms") // just in case
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("STATUS: " + response.statusCode());
System.out.println("BODY: " + response.body());
}
}

View File

@@ -1,39 +0,0 @@
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.spec.McpSchema;
import java.net.http.HttpRequest;
import java.time.Duration;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestMcpClient {
public static void main(String[] args) throws Exception {
String endpoint = "http://localhost:8086/mcp";
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
requestBuilder.header("Authorization", "Bearer dapms");
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(endpoint)
.requestBuilder(requestBuilder)
.connectTimeout(Duration.ofSeconds(5))
.build();
try (McpSyncClient client = McpClient.sync(transport)
.clientInfo(new McpSchema.Implementation("test-client", "1.0.0"))
.requestTimeout(Duration.ofSeconds(30))
.build()) {
System.out.println("Initializing client...");
client.initialize();
System.out.println("Client initialized.");
System.out.println("Calling tool...");
McpSchema.CallToolResult result = client.callTool(McpSchema.CallToolRequest.builder()
.name("iam_team_contact")
.arguments(Map.of("target", "전체"))
.build());
System.out.println("Result: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}

Binary file not shown.

View File

@@ -1,11 +0,0 @@
import java.lang.reflect.Method;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
public class TestReflection {
public static void main(String[] args) throws Exception {
Class<?> clazz = HttpServletStreamableServerTransportProvider.Builder.class;
for (Method m : clazz.getDeclaredMethods()) {
System.out.println(m.getName());
}
}
}

View File

@@ -1,20 +0,0 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class TestSseEndpoint {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8086/mcp"))
.header("Accept", "text/event-stream")
.header("X-Tool-Server-API-Key", "test")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("STATUS: " + response.statusCode());
System.out.println("BODY: " + response.body());
}
}

View File

@@ -1,181 +1,68 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.11'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.springframework.boot' version '4.0.5' apply false
id 'io.spring.dependency-management' version '1.1.6' apply false
}
// ============================================================================
// 전체 프로젝트 공통 Maven 좌표
// ============================================================================
allprojects {
// 모든 모듈이 공유하는 Maven Group ID입니다.
group = 'io.shinhanlife'
// 모든 모듈이 공유하는 프로젝트 버전입니다.
version = '0.0.1-SNAPSHOT'
}
subprojects {
// 모든 하위 모듈에서 Java Plugin을 사용합니다.
apply plugin: 'java'
// Spring Boot BOM 기반 의존성 버전 관리를 위해 적용합니다.
apply plugin: 'io.spring.dependency-management'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
sourceCompatibility = '21'
}
repositories {
// --------------------------------------------------------------------
// 외부 개발 환경
// --------------------------------------------------------------------
//
// 현재 인터넷이 가능한 환경에서는 Maven Central을 사용합니다.
//
// 신한라이프 폐쇄망 반입 시 아래 mavenCentral()은 제거하고
// 사내 Nexus Repository URL로 교체해야 합니다.
//
// 예:
//
// maven {
// url = uri('https://내부-Nexus/repository/maven-public/')
// }
//
// Nexus 인증이 필요한 경우에는 credentials 설정도 추가해야 합니다.
//
// credentials {
// username = findProperty('nexusUsername') ?: System.getenv('NEXUS_USERNAME')
// password = findProperty('nexusPassword') ?: System.getenv('NEXUS_PASSWORD')
// }
//
// 계정/비밀번호를 build.gradle에 직접 작성하면 안 됩니다.
// --------------------------------------------------------------------
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
// ========================================================================
// Dependency Version Management
// ========================================================================
dependencyManagement {
imports {
// Spring Boot 3.5.11이 검증한
// Spring Framework / Jackson / Tomcat / Logback /
// JUnit 등 주요 라이브러리 버전을 공통 관리합니다.
// 하위 모듈에서는 Spring Boot가 관리하는 라이브러리라면
// 가급적 개별 버전을 직접 지정하지 않고 사용합니다.
mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
// Gateway에서 사용하는 Spring AI 의존성 버전 관리
mavenBom 'org.springframework.ai:spring-ai-bom:1.1.8'
// MCP SDK 버전 관리
mavenBom 'io.modelcontextprotocol.sdk:mcp-bom:2.0.0'
mavenBom "org.springframework.ai:spring-ai-bom:2.0.0"
}
}
// ========================================================================
// Common Dependencies
// ========================================================================
dependencies {
// ====================================================================
// Lombok
// ====================================================================
// Lombok은 컴파일 시 Getter / Setter / Builder 등의
// 반복 코드를 생성합니다.
//
// compileOnly이므로 Runtime JAR에는 포함되지 않습니다.
compileOnly 'org.projectlombok:lombok'
// Lombok Annotation Processor입니다.
annotationProcessor 'org.projectlombok:lombok'
// 테스트 코드에서도 Lombok Annotation을 사용할 수 있도록 합니다.
testCompileOnly 'org.projectlombok:lombok'
// 테스트 코드용 Lombok Annotation Processor입니다.
testAnnotationProcessor 'org.projectlombok:lombok'
// ====================================================================
// MapStruct
// ====================================================================
// Tool DTO와
// MCI 요청/응답 DTO 사이의 Converter 구현체를
// 컴파일 시 자동 생성하기 위해 사용합니다.
implementation 'org.mapstruct:mapstruct:1.6.3'
// MapStruct Converter 구현 클래스를 생성하는
// Annotation Processor입니다.
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
// ====================================================================
// Test
// ====================================================================
// Spring Boot Test / JUnit 5 / Mockito / AssertJ 등
// 공통 테스트 환경을 제공합니다.
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Gradle / IDE 환경에서 JUnit Platform 기반 테스트 실행을 지원합니다.
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
// ========================================================================
// Java Compile Options
// ========================================================================
tasks.withType(JavaCompile).configureEach {
// Reflection 및 MCP Schema 생성 시
// Java 메서드의 실제 파라미터명을 사용할 수 있도록
// 컴파일 결과에 파라미터 이름을 보존합니다.
tasks.withType(JavaCompile) {
options.compilerArgs << '-parameters'
// MapStruct가 생성하는 Converter 구현 클래스를
// Spring Bean(@Component)으로 생성합니다.
//
// 이를 통해 생성자 주입 등의 방식으로 사용할 수 있습니다.
options.compilerArgs << '-Amapstruct.defaultComponentModel=spring'
}
// ========================================================================
// Test Configuration
// ========================================================================
tasks.withType(Test).configureEach {
// 모든 하위 모듈에서 JUnit 5 Platform을 사용합니다.
tasks.withType(Test) {
useJUnitPlatform()
}
}
task exportAllLibs(type: Copy) {
into 'all-exported-libs'
subprojects {
from(it.configurations.findByName('runtimeClasspath'))
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
def toolCoreProject = project(':dap-was-lib')
tasks.register('validateMcpToolNames', JavaExec) {
group = 'verification'
description = 'Checks duplicate @McpFunction names across all Tool modules before packaging.'
dependsOn toolCoreProject.tasks.named('classes')
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
mainClass.set('io.shinhanlife.dap.lib.validation.McpToolNameValidationRunner')
args rootProject.projectDir.absolutePath
}
subprojects {
tasks.matching { it.name == 'bootJar' }.configureEach {
dependsOn rootProject.tasks.named('validateMcpToolNames')
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
Manifest-Version: 1.0

39
dap-gateway/build.gradle Normal file
View File

@@ -0,0 +1,39 @@
plugins {
id 'org.springframework.boot'
}
dependencies {
implementation project(':dap-was-lib')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Spring AI MCP Server
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
implementation 'org.springframework.ai:spring-ai-openai:2.0.0'
// MyBatis & DB
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
runtimeOnly 'com.h2database:h2'
implementation 'p6spy:p6spy:3.9.1'
// MapStruct
implementation "org.mapstruct:mapstruct:1.5.5.Final"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final"
// implementation 'com.networknt:json-schema-validator:1.4.0' // Spring AI 내장 버전과 충돌 방지를 위해 주석 처리
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}

View File

@@ -1,9 +1,9 @@
package io.shinhanlife.dat.mcg;
package io.shinhanlife.dap.mcg;
/**
* @package io.shinhanlife.dat.mcg
* @className DatGatewayApplication
* @package io.shinhanlife.dap.mcg
* @className DapGatewayApplication
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
@@ -21,12 +21,12 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dat.mcg", "io.shinhanlife.dat.lib.mcp", "io.shinhanlife.dat.lib.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dat.mcg", "io.shinhanlife.dat.lib.mcp", "io.shinhanlife.dat.lib.config"})
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcg", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcg", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config"})
@EnableCaching
@EnableScheduling
public class DatGatewayApplication {
public class DapGatewayApplication {
public static void main(String[] args) {
new org.springframework.boot.builder.SpringApplicationBuilder(DatGatewayApplication.class).headless(false).run(args);
SpringApplication.run(DapGatewayApplication.class, args);
}
}

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dat.mcg.aop;
package io.shinhanlife.dap.mcg.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
/**
* @package io.shinhanlife.dat.mcg.aop
* @package io.shinhanlife.dap.mcg.aop
* @className GatewayLoggingAspect
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -27,7 +27,7 @@ import org.springframework.util.StopWatch;
public class GatewayLoggingAspect {
// gateway의 controller 패키지 하위의 모든 클래스/메서드 실행 작동
@Around("execution(* io.shinhanlife.dat.mcg.presentation..*(..))")
@Around("execution(* io.shinhanlife.dap.mcg.presentation..*(..))")
public Object logGatewayExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String targetMethod = joinPoint.getSignature().toShortString();
StopWatch stopWatch = new StopWatch();

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.audit;
package io.shinhanlife.dap.mcg.audit;
/**
* @package io.shinhanlife.dat.mcg.audit
* @package io.shinhanlife.dap.mcg.audit
* @className AuditLogService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -15,8 +15,8 @@ package io.shinhanlife.dat.mcg.audit;
*
* </pre>
*/
import io.shinhanlife.dat.mcg.config.McpGatewayProperties;
import io.shinhanlife.dat.mcg.security.McpRequestContext;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.config;
package io.shinhanlife.dap.mcg.config;
/**
* @package io.shinhanlife.dat.mcg.config
* @package io.shinhanlife.dap.mcg.config
* @className AgentResponseBudgetProperties
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dat.mcg.config;
package io.shinhanlife.dap.mcg.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -8,7 +8,7 @@ import java.util.HashMap;
import java.util.Map;
/**
* @package io.shinhanlife.dat.mcg.config
* @package io.shinhanlife.dap.mcg.config
* @className GatewayFallbackProperties
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.config;
package io.shinhanlife.dap.mcg.config;
/**
* @package io.shinhanlife.dat.mcg.config
* @package io.shinhanlife.dap.mcg.config
* @className McpGatewayProperties
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -37,8 +37,8 @@ public class McpGatewayProperties {
private Boolean agentClaimsRequired;
private Boolean trustedClaimsRequired;
private Boolean writeApprovalRequired;
private Boolean traceEnabled;
private Long traceRetentionSize;
private Boolean redisTraceEnabled;
private Long redisTraceTtlSeconds;
private Long toolTimeoutMillis;
private Integer retryMaxAttempts;
private Long retryInitialBackoffMillis;
@@ -70,8 +70,8 @@ public class McpGatewayProperties {
public void setAgentClaimsRequired(Boolean agentClaimsRequired) { this.agentClaimsRequired = agentClaimsRequired; }
public void setTrustedClaimsRequired(Boolean trustedClaimsRequired) { this.trustedClaimsRequired = trustedClaimsRequired; }
public void setWriteApprovalRequired(Boolean writeApprovalRequired) { this.writeApprovalRequired = writeApprovalRequired; }
public void setTraceEnabled(Boolean traceEnabled) { this.traceEnabled = traceEnabled; }
public void setTraceRetentionSize(Long traceRetentionSize) { this.traceRetentionSize = traceRetentionSize; }
public void setRedisTraceEnabled(Boolean redisTraceEnabled) { this.redisTraceEnabled = redisTraceEnabled; }
public void setRedisTraceTtlSeconds(Long redisTraceTtlSeconds) { this.redisTraceTtlSeconds = redisTraceTtlSeconds; }
public void setToolTimeoutMillis(Long toolTimeoutMillis) { this.toolTimeoutMillis = toolTimeoutMillis; }
public void setRetryMaxAttempts(Integer retryMaxAttempts) { this.retryMaxAttempts = retryMaxAttempts; }
public void setRetryInitialBackoffMillis(Long retryInitialBackoffMillis) { this.retryInitialBackoffMillis = retryInitialBackoffMillis; }
@@ -103,8 +103,8 @@ public class McpGatewayProperties {
public boolean agentClaimsRequired() { return agentClaimsRequired == null || agentClaimsRequired; }
public boolean trustedClaimsRequired() { return trustedClaimsRequired == null || trustedClaimsRequired; }
public boolean writeApprovalRequired() { return writeApprovalRequired == null || writeApprovalRequired; }
public boolean traceEnabled() { return traceEnabled == null || traceEnabled; }
public long traceRetentionSize() { return traceRetentionSize == null || traceRetentionSize < 1 ? 1000 : traceRetentionSize; }
public boolean redisTraceEnabled() { return redisTraceEnabled == null || redisTraceEnabled; }
public long redisTraceTtlSeconds() { return redisTraceTtlSeconds == null || redisTraceTtlSeconds < 1 ? 3_600 : redisTraceTtlSeconds; }
public long toolTimeoutMillis() { return toolTimeoutMillis == null || toolTimeoutMillis < 1 ? 5_000 : toolTimeoutMillis; }
public int retryMaxAttempts() { return retryMaxAttempts == null || retryMaxAttempts < 1 ? 3 : retryMaxAttempts; }
public long retryInitialBackoffMillis() { return retryInitialBackoffMillis == null || retryInitialBackoffMillis < 1 ? 1_000 : retryInitialBackoffMillis; }

View File

@@ -0,0 +1,46 @@
package io.shinhanlife.dap.mcg.config;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @package io.shinhanlife.dap.mcg.config
* @className RedisConfig
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, ToolMetadata> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, ToolMetadata> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 1. Key는 무조건 String
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 2. Value는 Generic 직렬화기 사용 (생성자 인자 없음)
Jackson2JsonRedisSerializer<ToolMetadata> serializer = new Jackson2JsonRedisSerializer<>(ToolMetadata.class);
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.guardrail;
package io.shinhanlife.dap.mcg.guardrail;
/**
* @package io.shinhanlife.dat.mcg.guardrail
* @package io.shinhanlife.dap.mcg.guardrail
* @className GuardrailService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -15,9 +15,9 @@ package io.shinhanlife.dat.mcg.guardrail;
*
* </pre>
*/
import io.shinhanlife.dat.mcg.resilience.FailureType;
import io.shinhanlife.dat.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Service;

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.guardrail;
package io.shinhanlife.dap.mcg.guardrail;
/**
* @package io.shinhanlife.dat.mcg.guardrail
* @package io.shinhanlife.dap.mcg.guardrail
* @className ToolResponseGuardrailService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -15,9 +15,9 @@ package io.shinhanlife.dat.mcg.guardrail;
*
* </pre>
*/
import io.shinhanlife.dat.mcg.resilience.FailureType;
import io.shinhanlife.dat.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dat.mcg.messaging;
package io.shinhanlife.dap.mcg.messaging;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -10,7 +10,7 @@ import java.util.Map;
import java.util.UUID;
/**
* @package io.shinhanlife.dat.mcg.messaging
* @package io.shinhanlife.dap.mcg.messaging
* @className KafkaProducerService
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.presentation;
package io.shinhanlife.dap.mcg.presentation;
/**
* @package io.shinhanlife.dat.mcg.presentation
* @package io.shinhanlife.dap.mcg.presentation
* @className ChatController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -15,10 +15,10 @@ package io.shinhanlife.dat.mcg.presentation;
*
* </pre>
*/
import io.shinhanlife.dat.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcg.registry.InMemoryRegistryService;
import io.shinhanlife.dat.mcg.service.ExecuteService;
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
@@ -41,19 +41,10 @@ public class ChatController {
private final ExecuteService executeService;
private final ObjectMapper objectMapper;
private final InMemoryRegistryService registryService;
private final RedisRegistryService registryService;
private final ChatClient.Builder chatClientBuilder;
private final McpRouterController mcpRouterController;
@org.springframework.beans.factory.annotation.Value("${shinhan.ai.base-url}")
private String liteLlmBaseUrl;
@org.springframework.beans.factory.annotation.Value("${shinhan.ai.gemma-key}")
private String gemmaKey;
@org.springframework.beans.factory.annotation.Value("${shinhan.ai.qwen-key}")
private String qwenKey;
@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody Map<String, String> request,
@RequestHeader(value = "X-Agent-Id", required = false) String agentId,
@@ -70,18 +61,12 @@ public class ChatController {
if (toolsResponse != null && toolsResponse.getResult() instanceof Map) {
Map<String, Object> resultMap = (Map<String, Object>) toolsResponse.getResult();
if (resultMap.containsKey("tools")) {
List<Map<String, Object>> allTools = (List<Map<String, Object>>) resultMap.get("tools");
List<ToolMetadata> allTools = (List<ToolMetadata>) resultMap.get("tools");
Set<String> addedToolNames = new HashSet<>();
for (Map<String, Object> metaMap : allTools) {
String name = (String) metaMap.get("name");
if (name != null) {
String cleanName = name.replaceAll("[^a-zA-Z0-9_-]", "_");
for (ToolMetadata meta : allTools) {
if (Boolean.TRUE.equals(meta.getVisible()) && meta.getName() != null) {
String cleanName = meta.getName().replaceAll("[^a-zA-Z0-9_-]", "_");
if (!addedToolNames.contains(cleanName)) {
ToolMetadata meta = ToolMetadata.builder()
.name(name)
.description((String) metaMap.get("description"))
.parametersSchema((Map<String, Object>) metaMap.get("inputSchema"))
.build();
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
addedToolNames.add(cleanName);
}
@@ -90,59 +75,27 @@ public class ChatController {
}
}
String selectedModel = request.getOrDefault("model", "Qwen3-Coder").trim();
String selectedModel = request.getOrDefault("model", "gemini-flash-latest").trim();
if (selectedModel.isEmpty()) {
selectedModel = "Qwen3-Coder";
selectedModel = "gemini-flash-latest";
}
// LiteLLM 내부망 모델 매핑
// 1. gemini-flash-latest 선택 OpenRouter의 Google Gemma 4 모델 라우팅
if (selectedModel.equals("gemini-flash-latest")) {
selectedModel = "Gemma-4-31B";
log.info("[Real AI Chat] 화면의 Gemini 플래시 선택을 사내 Gemma-4-31B 모델로 라우팅 전환");
selectedModel = "google/gemma-4-31b-it:free";
log.info("[Real AI Chat] Gemini 직접 API → OpenRouter Google Gemma 4 31B 로 라우팅 전환");
}
ChatClient activeChatClient;
org.springframework.ai.chat.prompt.ChatOptions chatOptions;
// 2. 모델 분기 처리: 사내 모델(Qwen3/Gemma-4) vs 기존 외부 모델(OpenRouter )
if ("Qwen3-Coder".equalsIgnoreCase(selectedModel) || "Gemma-4-31B".equalsIgnoreCase(selectedModel)) {
// 사내 LiteLLM 환경 동적 ChatModel 생성
// Use liteLlmBaseUrl from application.yml
String apiKey = "Gemma-4-31B".equalsIgnoreCase(selectedModel) ? gemmaKey : qwenKey;
org.springframework.ai.openai.api.OpenAiApi openAiApi = org.springframework.ai.openai.api.OpenAiApi.builder()
.baseUrl(liteLlmBaseUrl)
.apiKey(new org.springframework.ai.model.SimpleApiKey(apiKey))
.build();
org.springframework.ai.openai.OpenAiChatModel dynamicChatModel = org.springframework.ai.openai.OpenAiChatModel.builder()
.openAiApi(openAiApi)
.build();
activeChatClient = ChatClient.builder(dynamicChatModel)
.defaultSystem("You are AX HUB Assistant, a highly capable enterprise AI agent. You must use the provided tools to answer user questions when necessary. Always answer politely in Korean.")
.build();
chatOptions = org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(selectedModel)
.temperature(0.2)
.maxTokens(16384)
.build();
} else {
// 기존 방식: application.yml 설정된 기본 Bean (OpenRouter ) 사용
activeChatClient = chatClientBuilder
.defaultSystem("You are AX HUB Assistant, a highly capable enterprise AI agent. You must use the provided tools to answer user questions when necessary. Always answer politely in Korean.")
.build();
chatOptions = org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(selectedModel)
.build();
}
// 2. OpenRouter 무료 모델 처리 (Spring AI 빌드된 ChatClient MCP 호출 완벽 지원!)
ChatClient activeChatClient = chatClientBuilder
.defaultSystem("You are AX HUB Assistant, a highly capable enterprise AI agent. You must use the provided tools to answer user questions when necessary. Always answer politely in Korean.")
.build();
Flux<String> responseStream = activeChatClient.prompt()
.user(message)
.toolCallbacks(callbacks.toArray(new ToolCallback[0]))
.options(chatOptions)
.tools((Object[]) callbacks.toArray(new ToolCallback[0])) // Spring AI 2.0 uses tools()
.options(org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(selectedModel))
.stream()
.content();

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dat.mcg.presentation;
package io.shinhanlife.dap.mcg.presentation;
import io.shinhanlife.dat.mcg.service.KillSwitchService;
import io.shinhanlife.dap.mcg.service.KillSwitchService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @package io.shinhanlife.dat.mcg.presentation
* @package io.shinhanlife.dap.mcg.presentation
* @className KillSwitchController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -36,9 +36,9 @@ public class KillSwitchController {
*/
@PostMapping("/{type}/{target}")
public ResponseEntity<?> toggleKillSwitch(
@PathVariable("type") String type,
@PathVariable("target") String target,
@RequestParam("state") boolean state) {
@PathVariable String type,
@PathVariable String target,
@RequestParam boolean state) {
killSwitchService.toggleKillSwitch(type, target, state);
@@ -55,8 +55,8 @@ public class KillSwitchController {
*/
@DeleteMapping("/{type}/{target}")
public ResponseEntity<?> removeKillSwitch(
@PathVariable("type") String type,
@PathVariable("target") String target) {
@PathVariable String type,
@PathVariable String target) {
killSwitchService.removeKillSwitch(type, target);

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.presentation;
package io.shinhanlife.dap.mcg.presentation;
/**
* @package io.shinhanlife.dat.mcg.presentation
* @package io.shinhanlife.dap.mcg.presentation
* @className McpRouterController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -16,15 +16,14 @@ package io.shinhanlife.dat.mcg.presentation;
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dat.lib.adapter.dto.JsonRpcRequest;
import io.shinhanlife.dat.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dat.lib.adapter.dto.Params;
import io.shinhanlife.dat.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dat.mcg.config.McpGatewayProperties;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcg.registry.InMemoryRegistryService;
import io.shinhanlife.dat.mcg.service.ExecuteService;
import io.shinhanlife.dat.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcRequest;
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.lib.adapter.dto.Params;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDateTime;
@@ -50,20 +49,19 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory;
@Tag(name = "MCP Router API", description = "AI Agent의 요청을 받아 Adapter 시스템으로 라우팅하는 게이트웨이 API")
public class McpRouterController {
private final InMemoryRegistryService registryService;
private final RedisRegistryService redisRegistryService;
private final ExecuteService executeService;
private final SecurityProperties securityProperties;
private final ObjectMapper objectMapper;
private final GatewayFallbackProperties gatewayFallbackProperties;
private final RestClient restClient;
public McpRouterController(InMemoryRegistryService registryService,
public McpRouterController(RedisRegistryService redisRegistryService,
ExecuteService executeService,
SecurityProperties securityProperties,
ObjectMapper objectMapper,
GatewayFallbackProperties gatewayFallbackProperties,
McpGatewayProperties mcpGatewayProperties) {
this.registryService = registryService;
GatewayFallbackProperties gatewayFallbackProperties) {
this.redisRegistryService = redisRegistryService;
this.executeService = executeService;
this.securityProperties = securityProperties;
this.objectMapper = objectMapper;
@@ -74,15 +72,7 @@ public class McpRouterController {
factory.setConnectTimeout(1000); // 연결 시도 타임아웃 1초
factory.setReadTimeout(1000); // 응답 대기 타임아웃 1초
String apiKey = mcpGatewayProperties.toolServerApiKey();
if (apiKey == null || apiKey.isEmpty()) {
apiKey = "tool-server-key";
}
this.restClient = RestClient.builder()
.requestFactory(factory)
.defaultHeader("X-Tool-Server-API-Key", apiKey)
.build();
this.restClient = RestClient.builder().requestFactory(factory).build();
}
@PostMapping("/tools/call")
@@ -123,16 +113,11 @@ public class McpRouterController {
}
private List<ToolMetadata> fetchAllActiveTools() {
List<ToolMetadata> activeTools = new ArrayList<>();
try {
activeTools.addAll(registryService.getAllTools()
.stream()
.filter(ToolMetadata::getVisible)
.collect(Collectors.toList()));
} catch (Exception exception) {
log.warn("Registry is unavailable. Fetching tools from configured fallback Tool Pods instead.");
}
List<ToolMetadata> activeTools = redisRegistryService.getAllTools()
.stream()
.filter(ToolMetadata::getVisible)
.collect(Collectors.toList());
Set<String> knownTools = activeTools.stream()
.map(ToolMetadata::getUid)
.collect(Collectors.toSet());
@@ -151,10 +136,9 @@ public class McpRouterController {
if (localTools != null) {
for (ToolMetadata t : localTools) {
if (Boolean.TRUE.equals(t.getVisible()) && !knownTools.contains(t.getUid())) {
if (!Boolean.TRUE.equals(t.getIsRegistered()) && Boolean.TRUE.equals(t.getVisible()) && !knownTools.contains(t.getUid())) {
activeTools.add(t);
knownTools.add(t.getUid());
registryService.saveTool(t);
}
}
}
@@ -165,11 +149,6 @@ public class McpRouterController {
return activeTools;
}
/** Report 화면처럼 내부 메타데이터 전체가 필요한 Gateway 컴포넌트에 활성 Tool 목록을 제공합니다. */
List<ToolMetadata> activeToolsForReport() {
return fetchAllActiveTools();
}
@GetMapping("/tools/list")
public ResponseEntity<JsonRpcResponse> listTools(
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
@@ -182,40 +161,9 @@ public class McpRouterController {
.collect(Collectors.toList());
}
List<Map<String, Object>> mappedTools = activeTools.stream().map(t -> {
Map<String, Object> toolMap = new HashMap<>();
toolMap.put("name", t.getName());
toolMap.put("description", t.getDescription());
toolMap.put("inputSchema", t.getParametersSchema());
toolMap.put("outputSchema", t.getOutputSchema());
toolMap.put("title", t.getDisplayName());
Map<String, Object> annotations = new HashMap<>();
annotations.put("readOnlyHint", t.getReadOnlyHint() != null ? t.getReadOnlyHint() : false);
annotations.put("destructiveHint", t.getDestructiveHint() != null ? t.getDestructiveHint() : true);
annotations.put("idempotentHint", t.getIdempotentHint() != null ? t.getIdempotentHint() : false);
toolMap.put("annotations", annotations);
Map<String, Object> meta = new HashMap<>();
meta.put("version", t.getSemver());
meta.put("category_key", t.getCategoryKey());
meta.put("tags", t.getTags());
meta.put("owner_org", t.getOwnerOrg());
meta.put("legacy_interface_id", t.getMciServiceId());
meta.put("display_description", t.getDisplayDescription());
meta.put("when_to_use", t.getWhenToUse());
meta.put("when_not_to_use", t.getWhenNotToUse());
meta.put("io_limits", t.getIoLimits());
meta.put("example_queries", t.getExampleQueries());
meta.put("required_env_keys", t.getRequiredEnvKeys());
toolMap.put("_meta", meta);
return toolMap;
}).collect(Collectors.toList());
JsonRpcResponse response = new JsonRpcResponse();
response.setId(UUID.randomUUID().toString());
response.setResult(Map.of("tools", mappedTools));
response.setResult(Map.of("tools", activeTools));
return ResponseEntity.ok(response);
}
@@ -287,15 +235,23 @@ public class McpRouterController {
@PostMapping("/registry/register")
public ResponseEntity<String> registerTool(@RequestBody ToolMetadata meta) {
registryService.saveTool(meta);
redisRegistryService.saveTool(meta);
return ResponseEntity.ok("Registered");
}
@PostMapping("/registry/deregister")
public ResponseEntity<String> deregisterTool(@RequestBody String uid) {
registryService.removeTool(uid);
redisRegistryService.removeTool(uid);
return ResponseEntity.ok("Deregistered");
}
}
@PostMapping("/registry/heartbeat")
public ResponseEntity<String> heartbeat(@RequestBody String uid) {
boolean success = redisRegistryService.refreshHeartbeat(uid);
if (success) {
return ResponseEntity.ok("Heartbeat updated");
} else {
return ResponseEntity.status(404).body("Tool not found");
}
}
}

View File

@@ -0,0 +1,108 @@
package io.shinhanlife.dap.mcg.presentation;
/**
* @package io.shinhanlife.dap.mcg.presentation
* @className ScaffoldingController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.util.PodScaffolder;
import io.shinhanlife.dap.lib.util.ToolScaffolder;
import io.shinhanlife.dap.lib.util.ToolSourceUpdater;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/scaffold")
public class ScaffoldingController {
@PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) {
try {
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
if (!moduleName.startsWith("dap-tool-")) moduleName = "dap-tool-" + moduleName;
String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date");
if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
return PodScaffolder.scaffoldPod(moduleName, port, shortName, author, date);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool")
public String scaffoldTool(@RequestBody Map<String, String> req) {
try {
String baseName = req.get("baseName");
String interfaceId = req.get("interfaceId");
String description = req.get("description");
String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON"));
String routingType = req.getOrDefault("routingType", "HTTP");
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
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, clientSystemCode);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool/update")
public String updateTool(@RequestBody Map<String, String> req) {
try {
String toolName = req.get("toolName");
String domainGroup = req.getOrDefault("categoryKey", req.get("domainGroup"));
String description = req.get("description");
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
Boolean requiresApproval = req.containsKey("requiresApproval") ? Boolean.parseBoolean(req.get("requiresApproval")) : null;
ToolSourceUpdater.updateToolSource(toolName, domainGroup, description, register, requiresApproval);
return "성공";
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@GetMapping("/modules")
public List<String> listModules() {
try {
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null) sourceDir = System.getProperty("user.dir");
File dir = new File(sourceDir);
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-was-lib"));
if (files == null || files.length == 0) {
return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms");
}
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) {
return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms");
}
}
}

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.trace;
package io.shinhanlife.dap.mcg.redis;
/**
* @package io.shinhanlife.dat.mcg.trace
* @package io.shinhanlife.dap.mcg.redis
* @className McpMonitorEventService
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -0,0 +1,264 @@
package io.shinhanlife.dap.mcg.redis;
/**
* @package io.shinhanlife.dap.mcg.redis
* @className RedisToolTraceService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class RedisToolTraceService {
private static final Logger log = LoggerFactory.getLogger(RedisToolTraceService.class);
private static final String KEY_PREFIX = "mcp:request:";
private static final String RECENT_KEY = "mcp:request:recent";
private final McpGatewayProperties properties;
private final ObjectProvider<StringRedisTemplate> redisProvider;
private final ObjectMapper json;
private final McpMonitorEventService monitorEvents;
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
public RedisToolTraceService(McpGatewayProperties properties,
ObjectProvider<StringRedisTemplate> redisProvider,
ObjectMapper json,
McpMonitorEventService monitorEvents) {
this.properties = properties;
this.redisProvider = redisProvider;
this.json = json;
this.monitorEvents = monitorEvents;
}
/**
* Agent request has entered the MCP tool gateway.
*/
public void started(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments) {
save(context, metadata, arguments, "STARTED", 0, "", 0, 0, 0,
"Agent request entered MCP", "");
}
/**
* MCP is attempting to call the target Tool server.
*/
public void attemptStarted(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
int attempt, int maxAttempts) {
save(context, metadata, arguments, "ATTEMPTING", 0, "", attempt, maxAttempts, 0,
"Calling tool server", "");
}
/**
* MCP will retry the Tool call after backoff.
*/
public void retryWaiting(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
int failedAttempt, int maxAttempts, long backoffMillis, String failureType) {
save(context, metadata, arguments, "RETRY_WAITING", 0, failureType, failedAttempt, maxAttempts, backoffMillis,
"Retry will be attempted after backoff", "");
}
/**
* Tool execution has finished.
*/
public void finished(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments,
long elapsedMillis, boolean success, String failureType, String responseText) {
save(context, metadata, arguments, success ? "SUCCESS" : "FAILED", elapsedMillis, failureType, 0, 0, 0,
success ? "Tool call completed" : "Tool call failed", responseText);
}
/**
* Recent live trace entries. One current entry is kept per request id.
*/
public List<String> recent() {
if (!properties.redisTraceEnabled()) {
return List.of("Redis trace is disabled. Set MCP_REDIS_TRACE_ENABLED=true.");
}
StringRedisTemplate redis = redisProvider.getIfAvailable();
if (redis == null) {
return List.of("RedisTemplate is not available.");
}
try {
List<String> requestIds = redis.opsForList().range(RECENT_KEY, 0, 49);
if (requestIds == null || requestIds.isEmpty()) {
return List.of();
}
return requestIds.stream()
.map(requestId -> redis.opsForValue().get(KEY_PREFIX + requestId))
.filter(value -> value != null && !value.isBlank())
.toList();
} catch (Exception error) {
log.warn("Redis trace read failed. message={}", error.getMessage());
return List.of("Redis trace read failed: " + error.getMessage());
}
}
/**
* Historical accumulation is intentionally disabled.
*
* The MCP monitor is used to observe the current Agent request flow only,
* so this method returns the same live entries as recent().
*/
public List<String> history() {
return recent();
}
private void save(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, String status,
long elapsedMillis, String failureType, int attempt, int maxAttempts,
long backoffMillis, String message, String responseText) {
String stateKey = stateKey(context, metadata);
if (attempt > 0 && maxAttempts > 0) {
attemptStates.put(stateKey, new AttemptState(attempt, maxAttempts));
}
String payload;
try {
payload = tracePayload(context, metadata, arguments, status, elapsedMillis, failureType, attempt,
maxAttempts, backoffMillis, message, responseText);
monitorEvents.publish(payload);
if ("SUCCESS".equals(status) || "FAILED".equals(status)) {
attemptStates.remove(stateKey);
}
} catch (Exception error) {
log.warn("MCP monitor event publish failed. requestId={} tool={} message={}",
context.requestId(), metadata.getName(), error.getMessage());
return;
}
if (!properties.redisTraceEnabled()) {
return;
}
StringRedisTemplate redis = redisProvider.getIfAvailable();
if (redis == null) {
return;
}
try {
String requestId = context.requestId();
Duration ttl = Duration.ofSeconds(properties.redisTraceTtlSeconds());
redis.opsForValue().set(KEY_PREFIX + requestId, payload, ttl);
redis.opsForList().remove(RECENT_KEY, 0, requestId);
redis.opsForList().leftPush(RECENT_KEY, requestId);
redis.opsForList().trim(RECENT_KEY, 0, 49);
redis.expire(RECENT_KEY, ttl);
} catch (Exception error) {
log.warn("Redis trace save failed. requestId={} tool={} message={}",
context.requestId(), metadata.getName(), error.getMessage());
}
}
String tracePayload(McpRequestContext context, ToolMetadata metadata, ObjectNode arguments, String status,
long elapsedMillis, String failureType, int attempt, int maxAttempts,
long backoffMillis, String message, String responseText) throws Exception {
AttemptState state = attemptStates.get(stateKey(context, metadata));
int displayAttempt = attempt > 0 ? attempt : state == null ? 0 : state.attempt();
int displayMaxAttempts = maxAttempts > 0 ? maxAttempts : state == null ? 0 : state.maxAttempts();
int retryCount = Math.max(0, displayAttempt - 1);
Map<String, Object> trace = new LinkedHashMap<>();
trace.put("requestId", context.requestId());
trace.put("traceGroupId", context.traceGroupId());
trace.put("agentId", context.agentId());
trace.put("userId", context.userId());
trace.put("clientAddress", context.clientAddress());
trace.put("toolName", metadata.getName());
trace.put("operationType", metadata.getOperationType() != null ? metadata.getOperationType().name() : "READ");
trace.put("status", status);
trace.put("message", message == null ? "" : message);
trace.put("failureType", failureType == null ? "" : failureType);
trace.put("attempt", displayAttempt);
trace.put("maxAttempts", displayMaxAttempts);
trace.put("retryCount", retryCount);
trace.put("backoffMillis", backoffMillis);
trace.put("elapsedMillis", elapsedMillis);
trace.put("idempotencyKeyHash", hashText(arguments.path("idempotencyKey").asText("")));
List<String> argNames = new ArrayList<>();
arguments.fieldNames().forEachRemaining(argNames::add);
trace.put("argumentNames", argNames);
trace.put("arguments", arguments.toString());
trace.put("responseSummary", responseSummary(responseText));
trace.put("timestamp", Instant.now().toString());
return json.writeValueAsString(trace);
}
/**
* Redis Trace에는 Tool 응답 원문을 저장하지 않고 size/hash/상태/카운트만 저장합니다.
*/
Map<String, Object> responseSummary(String responseText) {
Map<String, Object> summary = new LinkedHashMap<>();
if (responseText == null || responseText.isBlank()) {
summary.put("bytes", 0);
summary.put("sha256", "");
return summary;
}
byte[] bytes = responseText.getBytes(StandardCharsets.UTF_8);
summary.put("bytes", bytes.length);
summary.put("sha256", sha256(bytes));
try {
JsonNode root = json.readTree(responseText);
summary.put("success", root.path("success").asBoolean(false));
summary.put("status", root.path("status").asText(""));
summary.put("toolName", root.path("toolName").asText(""));
summary.put("pageSize", root.path("pageSize").asInt(0));
summary.put("pageCount", root.path("pageCount").asInt(0));
summary.put("returnedCount", root.path("returnedCount").asInt(0));
summary.put("totalCount", root.path("totalCount").asLong(0));
summary.put("hasMore", root.path("hasMore").asBoolean(false));
summary.put("nextCursor", root.path("nextCursor").asText(""));
summary.put("message", root.path("message").asText(""));
summary.put("resultRef", root.path("resultRef").asText(""));
} catch (Exception ignored) {
summary.put("parseable", false);
}
return summary;
}
private String sha256(byte[] bytes) {
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
} catch (Exception error) {
return "";
}
}
private String hashText(String value) {
if (value == null || value.isBlank()) {
return "";
}
return sha256(value.getBytes(StandardCharsets.UTF_8));
}
private String stateKey(McpRequestContext context, ToolMetadata metadata) {
return context.requestId() + ":" + metadata.getName();
}
private record AttemptState(int attempt, int maxAttempts) {
}
}

View File

@@ -0,0 +1,110 @@
package io.shinhanlife.dap.mcg.registry;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.Objects;
/**
* @package io.shinhanlife.dap.mcg.registry
* @className RedisRegistryService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RedisRegistryService {
private final RedisTemplate<String, ToolMetadata> redisTemplate;
private static final String KEY_PREFIX = "mcp:tool:";
private static final Duration DEFAULT_TTL = Duration.ofSeconds(45); // 실무 하트비트 주기 반영
/**
* 툴 등록 및 갱신 (TTL 기반으로 60초 뒤 자동 만료)
*/
public void saveTool(ToolMetadata meta) {
String key = KEY_PREFIX + meta.getUid();
meta.setLastHeartbeat(System.currentTimeMillis());
redisTemplate.opsForValue().set(key, meta, DEFAULT_TTL);
log.info(" [RedisRegistry] 툴 등록 완료: {}", meta.getUid());
}
/**
* 하트비트 갱신 (TTL 초기화)
*/
public boolean refreshHeartbeat(String uid) {
String key = KEY_PREFIX + uid;
Boolean exists = redisTemplate.expire(key, DEFAULT_TTL);
if (Boolean.TRUE.equals(exists)) {
log.debug(" [RedisRegistry] 하트비트 갱신: {}", uid);
return true;
} else {
log.warn(" [RedisRegistry] 존재하지 않는 툴에 대한 하트비트 요청: {}", uid);
return false;
}
}
public List<String> getAvailablePods(String uid) {
ToolMetadata tool = getTool(uid);
if (tool != null && tool.getPodUrl() != null) {
return List.of(tool.getPodUrl());
}
return List.of();
}
/**
* 실행 시 툴 정보 조회 (Tool Execution 시 참조)
*/
public ToolMetadata getTool(String uid) {
return redisTemplate.opsForValue().get(KEY_PREFIX + uid);
}
/**
* 툴 이름으로 정보 조회
*/
public ToolMetadata getToolByName(String name) {
List<ToolMetadata> allTools = getAllTools();
for (ToolMetadata tool : allTools) {
if (name.equals(tool.getName())) {
return tool;
}
}
return null;
}
/**
* 등록된 모든 활성 툴 목록 조회
*/
public List<ToolMetadata> getAllTools() {
Set<String> keys = redisTemplate.keys(KEY_PREFIX + "*");
if (keys == null || keys.isEmpty()) {
return List.of();
}
List<ToolMetadata> tools = redisTemplate.opsForValue().multiGet(keys);
return tools != null ? tools.stream().filter(Objects::nonNull).collect(Collectors.toList()) : List.of();
}
/**
* 툴 명시적 제거 (Deregister)
*/
public void removeTool(String uid) {
redisTemplate.delete(KEY_PREFIX + uid);
log.info(" [RedisRegistry] 툴 삭제 완료: {}", uid);
}
}

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dat.mcg.resilience
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreaker
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dat.mcg.resilience
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerMonitorController
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dat.mcg.resilience
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -15,7 +15,7 @@ package io.shinhanlife.dat.mcg.resilience;
*
* </pre>
*/
import io.shinhanlife.dat.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import org.springframework.stereotype.Service;
import java.time.Clock;

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dat.mcg.resilience
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerSnapshot
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dat.mcg.resilience
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerState
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dat.mcg.resilience
* @package io.shinhanlife.dap.mcg.resilience
* @className FailureType
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dat.mcg.resilience
* @package io.shinhanlife.dap.mcg.resilience
* @className RetryPolicy
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.resilience;
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dat.mcg.resilience
* @package io.shinhanlife.dap.mcg.resilience
* @className ToolExecutionException
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.security;
package io.shinhanlife.dap.mcg.security;
/**
* @package io.shinhanlife.dat.mcg.security
* @package io.shinhanlife.dap.mcg.security
* @className McpRequestContext
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.security;
package io.shinhanlife.dap.mcg.security;
/**
* @package io.shinhanlife.dat.mcg.security
* @package io.shinhanlife.dap.mcg.security
* @className McpRequestContextResolver
* @description AX HUB 시스템 처리 클래스
* @author 0986406

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.security;
package io.shinhanlife.dap.mcg.security;
/**
* @package io.shinhanlife.dat.mcg.security
* @package io.shinhanlife.dap.mcg.security
* @className ToolAuthorizationService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -15,11 +15,11 @@ package io.shinhanlife.dat.mcg.security;
*
* </pre>
*/
import io.shinhanlife.dat.mcg.config.McpGatewayProperties;
import io.shinhanlife.dat.mcg.resilience.FailureType;
import io.shinhanlife.dat.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dat.lib.dto.OperationType;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.OperationType;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import java.util.Set;

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.service;
package io.shinhanlife.dap.mcg.service;
/**
* @package io.shinhanlife.dat.mcg.service
* @package io.shinhanlife.dap.mcg.service
* @className ExecuteService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -17,20 +17,20 @@ package io.shinhanlife.dat.mcg.service;
*/
import java.util.HashMap;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcg.resilience.FailureType;
import io.shinhanlife.dat.mcg.resilience.RetryPolicy;
import io.shinhanlife.dat.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dat.lib.dto.OperationType;
import io.shinhanlife.dat.mcg.config.McpGatewayProperties;
import io.shinhanlife.dat.mcg.guardrail.GuardrailService;
import io.shinhanlife.dat.mcg.security.McpRequestContext;
import io.shinhanlife.dat.mcg.security.McpRequestContextResolver;
import io.shinhanlife.dat.mcg.audit.AuditLogService;
import io.shinhanlife.dat.mcg.resilience.CircuitBreaker;
import io.shinhanlife.dat.mcg.resilience.CircuitBreakerService;
import io.shinhanlife.dat.mcg.security.ToolAuthorizationService;
import io.shinhanlife.dat.mcg.trace.InMemoryToolTraceService;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.RetryPolicy;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.OperationType;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.GuardrailService;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.mcg.security.McpRequestContextResolver;
import io.shinhanlife.dap.mcg.audit.AuditLogService;
import io.shinhanlife.dap.mcg.resilience.CircuitBreaker;
import io.shinhanlife.dap.mcg.resilience.CircuitBreakerService;
import io.shinhanlife.dap.mcg.security.ToolAuthorizationService;
import io.shinhanlife.dap.mcg.redis.RedisToolTraceService;
import jakarta.annotation.PreDestroy;
import java.util.Map;
@@ -43,12 +43,12 @@ import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.shinhanlife.dat.mcg.tool.large.LargeToolResponseService;
import io.shinhanlife.dat.mcg.tool.large.PaginationRequestValidator;
import io.shinhanlife.dat.mcg.tool.result.ToolExecutionResultFormatter;
import io.shinhanlife.dat.mcg.tool.result.ToolExecutionResult;
import io.shinhanlife.dat.mcg.guardrail.ToolResponseGuardrailService;
import io.shinhanlife.dat.mcg.transport.ToolInvoker;
import io.shinhanlife.dap.mcg.tool.large.LargeToolResponseService;
import io.shinhanlife.dap.mcg.tool.large.PaginationRequestValidator;
import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResultFormatter;
import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
import io.shinhanlife.dap.mcg.guardrail.ToolResponseGuardrailService;
import io.shinhanlife.dap.mcg.transport.ToolInvoker;
@Slf4j
@Service
@@ -62,7 +62,7 @@ public class ExecuteService {
private final AuditLogService auditLogService;
private final CircuitBreakerService circuitBreakerService;
private final ToolAuthorizationService authorizationService;
private final InMemoryToolTraceService traceService;
private final RedisToolTraceService redisTrace;
private final McpGatewayProperties properties;
private final LargeToolResponseService largeResponses;
private final PaginationRequestValidator paginationValidator;
@@ -79,7 +79,7 @@ public class ExecuteService {
AuditLogService auditLogService,
CircuitBreakerService circuitBreakerService,
ToolAuthorizationService authorizationService,
InMemoryToolTraceService traceService,
RedisToolTraceService redisTrace,
McpGatewayProperties properties,
LargeToolResponseService largeResponses,
PaginationRequestValidator paginationValidator,
@@ -94,7 +94,7 @@ public class ExecuteService {
this.auditLogService = auditLogService;
this.circuitBreakerService = circuitBreakerService;
this.authorizationService = authorizationService;
this.traceService = traceService;
this.redisTrace = redisTrace;
this.properties = properties;
this.largeResponses = largeResponses;
this.paginationValidator = paginationValidator;
@@ -133,7 +133,7 @@ public class ExecuteService {
long startedAt = System.nanoTime();
auditLogService.toolStarted(context, toolName, argumentsNode);
traceService.started(context, metadata, argumentsNode);
redisTrace.started(context, metadata, argumentsNode);
try {
authorizationService.authorize(context, metadata);
@@ -174,13 +174,13 @@ public class ExecuteService {
finalResult.put("original_size", originalSize);
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
traceService.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
return finalResult;
} catch (ToolExecutionException error) {
long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
traceService.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
@@ -194,7 +194,7 @@ public class ExecuteService {
} catch (Exception error) {
long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
traceService.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
@@ -225,7 +225,7 @@ public class ExecuteService {
ToolExecutionException lastError = null;
for (int attempt = 1; attempt <= retryPolicy.maxAttempts(); attempt++) {
try {
traceService.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts());
redisTrace.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts());
circuitBreaker.beforeCall();
Object result = executeOnce(context, metadata, arguments, payload);
circuitBreaker.recordSuccess();
@@ -237,7 +237,7 @@ public class ExecuteService {
throw error;
}
long backoffMillis = retryPolicy.backoffMillis(attempt);
traceService.retryWaiting(context, metadata, arguments, attempt, retryPolicy.maxAttempts(), backoffMillis,
redisTrace.retryWaiting(context, metadata, arguments, attempt, retryPolicy.maxAttempts(), backoffMillis,
error.failureType().name());
sleepBeforeRetry(metadata.getName(), attempt, backoffMillis, error.failureType());
}
@@ -261,11 +261,6 @@ public class ExecuteService {
Map<String, String> headers = new java.util.HashMap<>();
headers.put("trace-id", context.requestId());
headers.put("request-id", java.util.UUID.randomUUID().toString());
String apiKey = properties.toolServerApiKey();
if (apiKey == null || apiKey.isEmpty()) {
apiKey = "tool-server-key";
}
headers.put("X-Tool-Server-API-Key", apiKey);
ObjectNode pageArguments = paginationValidator.normalize(metadata, arguments);
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
@@ -342,18 +337,13 @@ public class ExecuteService {
if (!retryAllowedByOperation(metadata, arguments)) {
return RetryPolicy.disabled();
}
return new RetryPolicy(retryMaxAttempts(metadata, properties.retryMaxAttempts()), properties.retryInitialBackoffMillis(),
return new RetryPolicy(properties.retryMaxAttempts(), properties.retryInitialBackoffMillis(),
properties.retryBackoffMultiplier(), properties.retryMaxBackoffMillis());
}
static int retryMaxAttempts(ToolMetadata metadata, int gatewayDefault) {
Integer toolValue = metadata.getRetryMaxAttempts();
return toolValue != null && toolValue > 0 ? toolValue : gatewayDefault;
}
private boolean retryAllowedByOperation(ToolMetadata metadata, ObjectNode arguments) {
if (metadata.getOperationType() == OperationType.READ) {
return true;
return metadata.getRetryEnabled() != null ? metadata.getRetryEnabled() : true;
}
return arguments.hasNonNull("idempotencyKey") && !arguments.path("idempotencyKey").asText("").isBlank();
}

View File

@@ -1,30 +1,35 @@
package io.shinhanlife.dat.mcg.service;
package io.shinhanlife.dap.mcg.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @package io.shinhanlife.dat.mcg.service
* @package io.shinhanlife.dap.mcg.service
* @className KillSwitchService
* @description AX HUB 시스템 위치 서비스 (인메모리)
* @description AX HUB 시스템 처리 클래
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class KillSwitchService {
// 인메모리 맵으로 변경
private final Map<String, String> killSwitchMap = new ConcurrentHashMap<>();
// 기본 제공되는 StringRedisTemplate 사용
private final StringRedisTemplate stringRedisTemplate;
public void checkAgent(String tenantId) {
String key = "mcp:kill:agent:" + tenantId;
if ("true".equalsIgnoreCase(killSwitchMap.get(key))) {
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
log.warn(" [KillSwitch] 차단된 에이전트 접근 시도: {}", tenantId);
throw new SecurityException(" 비상 차단: 해당 테넌트(" + tenantId + ")의 접근이 관리자에 의해 차단되었습니다.");
}
@@ -34,7 +39,7 @@ public class KillSwitchService {
if (toolName == null || toolName.isBlank()) return;
String key = "mcp:kill:tool:" + toolName;
if ("true".equalsIgnoreCase(killSwitchMap.get(key))) {
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
log.warn(" [KillSwitch] 차단된 툴 실행 시도: {}", toolName);
throw new SecurityException(" 비상 차단: 해당 툴(" + toolName + ")의 실행이 관리자에 의해 차단되었습니다.");
}
@@ -44,7 +49,7 @@ public class KillSwitchService {
if (integrationType == null || integrationType.isBlank()) return;
String key = "mcp:kill:route:" + integrationType;
if ("true".equalsIgnoreCase(killSwitchMap.get(key))) {
if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
log.warn(" [KillSwitch] 차단된 라우트 통신 시도: {}", integrationType);
throw new SecurityException(" 비상 차단: 해당 레거시 라우트(" + integrationType + ")로의 통신이 관리자에 의해 차단되었습니다.");
}
@@ -53,14 +58,14 @@ public class KillSwitchService {
// 관리자 API용 토글 메서드
public void toggleKillSwitch(String type, String target, boolean state) {
String key = "mcp:kill:" + type + ":" + target;
killSwitchMap.put(key, String.valueOf(state));
stringRedisTemplate.opsForValue().set(key, String.valueOf(state));
log.info(" [KillSwitch] 상태 변경: {} -> {} (차단 상태: {})", type, target, state);
}
// 관리자 API용 삭제 메서드 ( 항목 삭제)
// 관리자 API용 삭제 메서드 (Redis 자체를 완전 삭제)
public void removeKillSwitch(String type, String target) {
String key = "mcp:kill:" + type + ":" + target;
killSwitchMap.remove(key);
stringRedisTemplate.delete(key);
log.info(" [KillSwitch] 차단 키 완전 삭제: {} -> {}", type, target);
}
}

View File

@@ -1,9 +1,9 @@
package io.shinhanlife.dat.mcg.service;
package io.shinhanlife.dap.mcg.service;
import io.shinhanlife.dat.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dat.mcg.registry.InMemoryRegistryService;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -12,7 +12,7 @@ import java.util.List;
import java.util.Map;
/**
* @package io.shinhanlife.dat.mcg.service
* @package io.shinhanlife.dap.mcg.service
* @className ToolPlanner
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -30,7 +30,7 @@ import java.util.Map;
@RequiredArgsConstructor
public class ToolPlanner {
private final InMemoryRegistryService InMemoryRegistryService;
private final RedisRegistryService redisRegistryService;
private final SecurityProperties securityProperties;
private final GatewayFallbackProperties fallbackProperties;
@@ -50,7 +50,7 @@ public class ToolPlanner {
// 2. RedisRegistry에서 해당 툴의 메타데이터 조회
// (실제로는 메타데이터가 실행 계획의 핵심이 됩니다)
var toolMetadata = InMemoryRegistryService.getToolByName(toolName);
var toolMetadata = redisRegistryService.getToolByName(toolName);
if (toolMetadata == null) {
log.warn(" [Planner] 등록되지 않은 툴 요청: {}. Fallback 라우팅 규칙을 확인합니다.", toolName);
@@ -58,7 +58,7 @@ public class ToolPlanner {
String fallbackPodUrl = fallbackProperties.getDefaultUrl();
if (fallbackProperties.getRoutes() != null) {
for (Map.Entry<String, String> entry : fallbackProperties.getRoutes().entrySet()) {
if (toolName.startsWith(entry.getKey() + "_")) {
if (toolName.contains(entry.getKey())) {
fallbackPodUrl = entry.getValue();
break;
}
@@ -81,7 +81,7 @@ public class ToolPlanner {
}
// 2-1. [신규] 도메인 그룹핑 기반 권한 검증
if (tenantId != null && !tenantId.equalsIgnoreCase("system") && toolMetadata.getCategoryKey() != null) {
if (tenantId != null && toolMetadata.getCategoryKey() != null) {
String normalizedTenantId = tenantId.toLowerCase();
List<String> allowedDomains = securityProperties.getTenantDomains().get(normalizedTenantId);

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dat.mcg.sync;
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dat.mcg.sync
* @package io.shinhanlife.dap.mcg.sync
* @className CustomWebMvcSseServerTransportProvider
* @description AX HUB MCP Gateway SSE 전송 제공자 - SSE 기반의 MCP 서버 트랜스포트를 구현하는 클래스
* @author 0986406
@@ -210,37 +210,22 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
log.debug("Sending message to SSE stream: {}", message.getClass().getName());
log.info("Sending message to SSE stream: " + message.getClass().getName());
try {
String json = objectMapper.writeValueAsString(message);
log.debug("Serialized message: {}", json);
log.info("Serialized message: " + json);
if (this.emitter != null) {
this.emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("message").data(json));
log.debug("Message successfully sent to SSE emitter");
log.info("Message successfully sent to SSE emitter");
// Custom 프로토콜: 1회 요청당 1응답 종료 (스트림을 닫아버림)
// 클라이언트가 번의 POST 응답을 받고 연결을 끊기 때문
// this.emitter.complete(); // MCP 표준 클라이언트 지원을 위해 스트림 강제 종료 제거
// this.emitter.complete();
}
} catch (Exception e) {
if (e.getClass().getSimpleName().contains("AsyncRequestNotUsableException") ||
(e.getCause() != null && e.getCause() instanceof java.io.IOException) ||
(e instanceof IllegalStateException && e.getMessage() != null && e.getMessage().contains("already completed"))) {
log.debug("SSE emitter is disconnected (expected in Warm Pool): {}", e.getMessage());
} else {
log.error("Error sending message to SSE emitter", e);
}
log.error("Error sending message to SSE emitter", e);
if (this.emitter != null) {
try {
this.emitter.completeWithError(e);
} catch (Exception ignored) {
// ignore already completed
} finally {
// 죽은 Emitter를 null로 초기화하여 다음 5초 주기 재시도하지 않도록 방지
// 클라이언트가 재연결하면 setEmitter( Emitter) 통해 덮어씌워짐
this.emitter = null;
}
this.emitter.completeWithError(e);
}
}
});

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.sync;
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dat.mcg.sync
* @package io.shinhanlife.dap.mcg.sync
* @className DynamicMcpController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -26,7 +26,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import io.shinhanlife.dat.mcg.sync.CustomWebMvcSseServerTransportProvider;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
@RestController
public class DynamicMcpController {

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.sync;
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dat.mcg.sync
* @package io.shinhanlife.dap.mcg.sync
* @className DynamicMcpServerManager
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -18,8 +18,8 @@ package io.shinhanlife.dat.mcg.sync;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcg.sync.CustomWebMvcSseServerTransportProvider;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@@ -51,17 +51,10 @@ public class DynamicMcpServerManager {
getOrCreateServer("common");
getOrCreateServer("hr");
getOrCreateServer("payment");
// Tool category names are not Pod names. Keep existing external category
// endpoints available while the SMS/OTH Pods transition to SAL/CUS.
getOrCreateServer("sms");
getOrCreateServer("oth");
getOrCreateServer("sal");
getOrCreateServer("email");
getOrCreateServer("cus");
getOrCreateServer("pro");
getOrCreateServer("sys");
getOrCreateServer("oth");
getOrCreateServer("sample");
getOrCreateServer("smp"); // smp 카테고리도 기본적으로 항상 열어두도록 추가
}
private McpSyncServer getOrCreateServer(String categoryKey) {
@@ -75,7 +68,7 @@ public class DynamicMcpServerManager {
CustomWebMvcSseServerTransportProvider transport = new CustomWebMvcSseServerTransportProvider(ssePath, msgPath, objectMapper);
McpSyncServer newServer = McpServer.sync(transport)
.serverInfo("dat-was-" + key, "1.0.0")
.serverInfo("DAP-Gateway-" + key, "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.sync;
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dat.mcg.sync
* @package io.shinhanlife.dap.mcg.sync
* @className RegistryMcpToolSpecificationFactory
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -15,12 +15,11 @@ package io.shinhanlife.dat.mcg.sync;
*
* </pre>
*/
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.lib.mcp.ToolMetadataMcpMapper;
import io.shinhanlife.dat.mcg.service.ExecuteService;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dat.mcg.tool.result.ToolExecutionResult;
import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -49,7 +48,17 @@ public class RegistryMcpToolSpecificationFactory {
* Registry Entry 하나를 MCP SDK의 stateless sync Tool specification으로 변환합니다.
*/
public McpServerFeatures.SyncToolSpecification create(ToolMetadata entry) {
McpSchema.Tool tool = ToolMetadataMcpMapper.toTool(entry);
McpSchema.Tool tool = McpSchema.Tool.builder()
.name(entry.getName())
.description(description(entry))
.inputSchema(inputSchema(entry))
.annotations(McpSchema.ToolAnnotations.builder()
.readOnlyHint(Boolean.TRUE.equals(entry.getReadOnlyHint()))
.destructiveHint(Boolean.TRUE.equals(entry.getDestructiveHint()))
.idempotentHint(Boolean.TRUE.equals(entry.getIdempotentHint()))
.openWorldHint(Boolean.TRUE.equals(entry.getOpenWorldHint()))
.build())
.build();
return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool)
@@ -138,20 +147,6 @@ public class RegistryMcpToolSpecificationFactory {
return schema;
}
@SuppressWarnings("unchecked")
private McpSchema.JsonSchema toJsonSchema(Map<String, Object> schema) {
return new McpSchema.JsonSchema(
String.valueOf(schema.getOrDefault("type", "object")),
schema.get("properties") instanceof Map<?, ?> properties
? (Map<String, Object>) properties : Map.of(),
schema.get("required") instanceof List<?> required
? (List<String>) required : List.of(),
schema.get("additionalProperties") instanceof Boolean additionalProperties
? additionalProperties : Boolean.TRUE,
schema.get("$defs") instanceof Map<?, ?> defs ? (Map<String, Object>) defs : Map.of(),
schema.get("definitions") instanceof Map<?, ?> definitions
? (Map<String, Object>) definitions : Map.of());
}
private String description(ToolMetadata entry) {
return entry.getDescription() == null || entry.getDescription().isBlank()
? entry.getName() + " Tool"

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dat.mcg.sync;
package io.shinhanlife.dap.mcg.sync;
import io.shinhanlife.dat.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcg.registry.InMemoryRegistryService;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,7 +19,7 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
* @package io.shinhanlife.dat.mcg.sync
* @package io.shinhanlife.dap.mcg.sync
* @className RegistryMcpToolSynchronizer
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -38,18 +38,18 @@ public class RegistryMcpToolSynchronizer {
private final ObjectProvider<McpSyncServer> mcpServerProvider;
private final DynamicMcpServerManager dynamicMcpServerManager;
private final InMemoryRegistryService InMemoryRegistryService;
private final RedisRegistryService redisRegistryService;
private final RegistryMcpToolSpecificationFactory specificationFactory;
private final Set<String> managedToolNames = new LinkedHashSet<>();
private final ReentrantLock lock = new ReentrantLock();
public RegistryMcpToolSynchronizer(ObjectProvider<McpSyncServer> mcpServerProvider,
DynamicMcpServerManager dynamicMcpServerManager,
InMemoryRegistryService InMemoryRegistryService,
RedisRegistryService redisRegistryService,
RegistryMcpToolSpecificationFactory specificationFactory) {
this.mcpServerProvider = mcpServerProvider;
this.dynamicMcpServerManager = dynamicMcpServerManager;
this.InMemoryRegistryService = InMemoryRegistryService;
this.redisRegistryService = redisRegistryService;
this.specificationFactory = specificationFactory;
}
@@ -66,7 +66,7 @@ public class RegistryMcpToolSynchronizer {
public void synchronize() {
lock.lock();
try {
List<ToolMetadata> activeEntries = InMemoryRegistryService.getAllTools()
List<ToolMetadata> activeEntries = redisRegistryService.getAllTools()
.stream().filter(ToolMetadata::getVisible).collect(Collectors.toList());
// 1. Dynamic MCP Server 동기화 (카테고리별)

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.tool.large;
package io.shinhanlife.dap.mcg.tool.large;
/**
* @package io.shinhanlife.dat.mcg.tool.large
* @package io.shinhanlife.dap.mcg.tool.large
* @className AgentResponseBudgetService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -18,7 +18,7 @@ package io.shinhanlife.dat.mcg.tool.large;
import java.util.Iterator;
import java.util.Map;
import io.shinhanlife.dat.mcg.config.AgentResponseBudgetProperties;
import io.shinhanlife.dap.mcg.config.AgentResponseBudgetProperties;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

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