1 Commits

Author SHA1 Message Date
jade
2723d12568 Refactor: rename ToolHint to GrowToolHint and update annotations 2026-08-13 16:20:00 +09:00
516 changed files with 9792 additions and 18828 deletions

View File

@@ -33,11 +33,7 @@
* 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다. * 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다.
* 신한라이프 표준 로그 기준 (Logback 설정): * 신한라이프 표준 로그 기준 (Logback 설정):
* **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dat-gateway/A01/`) * **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dap-gateway/A01/`)
* **로그 네이밍 규칙:** `${HOSTNAME}_코드명_yyyyMMdd.log` (예: `${HOSTNAME}_A01_20260722.log`) * **로그 네이밍 규칙:** `${HOSTNAME}_코드명_yyyyMMdd.log` (예: `${HOSTNAME}_A01_20260722.log`)
* **기본 로그 구분 코드:** 시스템 운영기록 가동기록의 경우 `A01`을 기본으로 사용한다. * **기본 로그 구분 코드:** 시스템 운영기록 가동기록의 경우 `A01`을 기본으로 사용한다.
* **호스트명 동적 할당:** Logback 설정 시 `<property name="HOSTNAME" value="${HOSTNAME}" />` 를 선언하여 사용한다. * **호스트명 동적 할당:** 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 update
apt-get install -y nodejs git openjdk-21-jdk rsync apt-get install -y nodejs git openjdk-21-jdk rsync
elif command -v apk &> /dev/null; then 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 fi
- name: Checkout Code - name: Checkout Code
@@ -59,7 +59,7 @@ jobs:
# 4. 마운트된 /app 디렉토리로 이동하여 호스트의 도커 컴포즈 제어! # 4. 마운트된 /app 디렉토리로 이동하여 호스트의 도커 컴포즈 제어!
cd /app cd /app
docker system prune -f 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 --pull always --remove-orphans gateway redis mci-mock dozzle was-sms was-oth
# 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 및 빌드 캐시 자동 소거 청소! # 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 및 빌드 캐시 자동 소거 청소!
docker image prune -a -f docker image prune -a -f

3
.gitignore vendored
View File

@@ -122,6 +122,3 @@ ehthumbs.db
# ===== ?섍꼍?ㅼ젙 (誘쇨컧?뺣낫 遺꾨━ ?? ===== # ===== ?섍꼍?ㅼ젙 (誘쇨컧?뺣낫 遺꾨━ ?? =====
# application-local.yml # application-local.yml
# application-secret.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);
}
}

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();
}
}
}

142
README.md
View File

@@ -1,27 +1,5 @@
# AX HUB MCP Tool Platform # AX HUB MCP Tool Platform
## Current Tool Pod modules
| 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` |
- 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 프로젝트입니다. AX HUB에서 AI Agent가 업무 Tool을 검색하고 호출할 수 있도록 Gateway와 독립 Tool Pod를 제공하는 멀티 모듈 Spring Boot 프로젝트입니다.
## 1. 현재 구성 ## 1. 현재 구성
@@ -30,19 +8,16 @@ AX HUB에서 AI Agent가 업무 Tool을 검색하고 호출할 수 있도록 Gat
MCP Client / AI Agent MCP Client / AI Agent
| |
v v
Gateway (dat-gateway, 8081) Gateway (dap-gateway, 8081)
- MCP 연결, Tool Registry, 인증·인가, 라우팅, 공통 화면 - MCP 연결, Tool Registry, 인증·인가, 라우팅, 공통 화면
| |
+-------------------------+ +-------------------------+
| | | |
v v v v
Sales Tool Pod (dat-was-sal, 8082) Customer Tool Pod (dat-was-cus, 8084) SMS Tool Pod (dap-was-sms, 8082) OTH Tool Pod (dap-was-oth, 8084)
- 영업/알림 업무 Tool - 고객/공통 업무 Tool - SMS 업무 Tool - 공통/업무/MCI 연계 Tool
- Tool Manifest - Tool Manifest - Tool Manifest - Tool Manifest
- Pod Test Console - Pod Test Console - Pod Test Console - Pod Test Console
|
+--> Process Tool Pod (dat-was-pro, 8085)
+--> System Tool Pod (dat-was-sys, 8086)
| |
v v
MCI / EAI / EIMS 등 레거시 연계 대상 MCI / EAI / EIMS 등 레거시 연계 대상
@@ -54,14 +29,12 @@ Gateway는 외부 MCP 진입점과 Tool 등록·라우팅을 담당하고, 실
| 모듈 | 역할 | 기본 포트 | | 모듈 | 역할 | 기본 포트 |
|---|---|---:| |---|---|---:|
| `dat-gateway` | MCP Gateway, Registry, 라우팅, Chat·Catalog·Tester·Scaffold 화면 | 8081 | | `dap-gateway` | MCP Gateway, Registry, 라우팅, Chat·Catalog·Tester·Scaffold 화면 | 8081 |
| `dat-was-lib` | Tool 공통 라이브러리: 어노테이션, Schema, Manifest, MCI/EAI 어댑터, 공통 Web·보안 기능 | - | | `dap-was-lib` | Tool 공통 라이브러리: 어노테이션, Schema, Manifest, MCI/EAI 어댑터, 공통 Web·보안 기능 | - |
| `dat-was-sal` | 영업/알림 업무 Tool Pod | 8082 | | `dap-was-sms` | SMS/알림 업무 Tool Pod | 8082 |
| `dat-was-cus` | 고객/공통 업무 Tool Pod | 8084 | | `dap-was-oth` | 공통/샘플/MCI 연계 업무 Tool Pod | 8084 |
| `dat-was-pro` | 프로세스 업무 Tool Pod | 8085 |
| `dat-was-sys` | 시스템 업무 Tool Pod | 8086 |
> 기존 `dap-tool-core`, `dap-tool-sms`, `dap-tool-oth` 명칭은 현재 각각 `dat-was-lib`, `dat-was-sal`, `dat-was-cus`로 변경되었습니다. > 기존 `dap-tool-core`, `dap-tool-sms`, `dap-tool-oth` 명칭은 현재 각각 `dap-was-lib`, `dap-was-sms`, `dap-was-oth`로 변경되었습니다.
## 3. 개발 환경 ## 3. 개발 환경
@@ -87,11 +60,9 @@ $env:OPENROUTER_API_KEY = '<발급받은-키>'
각 애플리케이션은 별도 터미널에서 실행합니다. 각 애플리케이션은 별도 터미널에서 실행합니다.
```powershell ```powershell
.\gradlew.bat :dat-gateway:bootRun .\gradlew.bat :dap-gateway:bootRun
.\gradlew.bat :dat-was-sal:bootRun .\gradlew.bat :dap-was-sms:bootRun
.\gradlew.bat :dat-was-cus:bootRun .\gradlew.bat :dap-was-oth:bootRun
.\gradlew.bat :dat-was-pro:bootRun
.\gradlew.bat :dat-was-sys:bootRun
``` ```
### 4.2 Docker Compose 실행 ### 4.2 Docker Compose 실행
@@ -103,10 +74,8 @@ docker compose up -d --build
| 서비스 | 호스트 URL | 컨테이너 포트 | | 서비스 | 호스트 URL | 컨테이너 포트 |
|---|---|---:| |---|---|---:|
| Gateway | http://localhost:8281 | 8081 | | Gateway | http://localhost:8281 | 8081 |
| Sales Tool Pod | http://localhost:8282 | 8082 | | SMS Tool Pod | http://localhost:8282 | 8082 |
| Customer Tool Pod | http://localhost:8284 | 8084 | | OTH 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 | | MCI Mock | http://localhost:8089 | 8080 |
| Dozzle 로그 화면 | http://localhost:8288 | 8080 | | Dozzle 로그 화면 | http://localhost:8288 | 8080 |
@@ -125,31 +94,14 @@ docker compose up -d --build
Docker Compose로 실행한 경우 Gateway 화면은 포트 `8281`을 사용합니다. Docker Compose로 실행한 경우 Gateway 화면은 포트 `8281`을 사용합니다.
### Pod Module New: 독립 프로젝트 생성
Scaffold 화면의 `Pod Module New` 탭은 기존 Pod Module 생성 흐름과 별도로 동작합니다. 새 모듈명과 포트, workspace 경로를 입력하면 해당 workspace 바로 아래에 독립 Tool Pod 프로젝트를 생성합니다.
기본 workspace는 `C:\eGovFrameDev-4.3.1-64bit\workspace`이며, 예를 들어 모듈명으로 `dat-was-payment`를 입력하면 `C:\eGovFrameDev-4.3.1-64bit\workspace\dat-was-payment` 프로젝트가 만들어집니다. 모듈명은 `dat-was-`로 시작해야 하며, 이미 같은 폴더가 있으면 덮어쓰지 않고 생성 요청을 거절합니다.
생성된 프로젝트는 같은 workspace의 `dat-lib-datmt`를 Gradle composite build로 참조합니다. 생성 직후 프로젝트 폴더에서 다음 명령으로 컴파일할 수 있습니다.
```powershell
cd C:\eGovFrameDev-4.3.1-64bit\workspace\dat-was-payment
.\gradlew.bat compileJava
```
이 기능은 기존 AX HUB 멀티 모듈 Pod나 기존 `Pod Module` 탭의 구성·소스를 수정하지 않습니다.
### Tool Pod Test Console ### Tool Pod Test Console
공통 정적 화면인 `tool-test-console.html`은 Tool Pod의 `/tool-manifest`를 읽어, 해당 Pod에 등록된 Tool과 `inputSchema`를 기준으로 요청 JSON을 만들어 직접 실행합니다. 공통 정적 화면인 `tool-test-console.html`은 Tool Pod의 `/tool-manifest`를 읽어, 해당 Pod에 등록된 Tool과 `inputSchema`를 기준으로 요청 JSON을 만들어 직접 실행합니다.
| 대상 Pod | 로컬 URL | Docker Compose URL | | 대상 Pod | 로컬 URL | Docker Compose URL |
|---|---|---| |---|---|---|
| Customer | http://localhost:8084/tool-test-console.html | http://localhost:8284/tool-test-console.html | | OTH | 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 | | SMS | 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 |
사용 방법은 다음과 같습니다. 사용 방법은 다음과 같습니다.
@@ -170,7 +122,7 @@ cd C:\eGovFrameDev-4.3.1-64bit\workspace\dat-was-payment
> Auto Tester의 더미 요청 성공은 **통신·등록·기본 스키마 확인용 스모크 테스트**입니다. 실제 MCI/EAI 업무 규칙, 권한, 데이터 정합성은 보장하지 않습니다. Stress Test와 Auto Ping은 실제 Tool·레거시 호출을 유발할 수 있으므로 개발/테스트 환경 및 승인된 테스트 데이터에서만 사용합니다. > Auto Tester의 더미 요청 성공은 **통신·등록·기본 스키마 확인용 스모크 테스트**입니다. 실제 MCI/EAI 업무 규칙, 권한, 데이터 정합성은 보장하지 않습니다. Stress Test와 Auto Ping은 실제 Tool·레거시 호출을 유발할 수 있으므로 개발/테스트 환경 및 승인된 테스트 데이터에서만 사용합니다.
UI에서 사용하는 Tailwind CSS와 Chart.js는 `dat-gateway/src/main/resources/static/lib`에 포함되어 있어, 화면 라이브러리 로딩을 위해 외부 CDN 연결이 필요하지 않습니다. UI에서 사용하는 Tailwind CSS와 Chart.js는 `dap-gateway/src/main/resources/static/lib`에 포함되어 있어, 화면 라이브러리 로딩을 위해 외부 CDN 연결이 필요하지 않습니다.
## 6. 주요 HTTP API ## 6. 주요 HTTP API
@@ -238,15 +190,15 @@ Request DTO
-> Legacy System -> Legacy System
``` ```
- Request DTO 필드에는 `@Schema(description = "...")`와 Bean Validation 어노테이션을 선언합니다. - Request DTO에는 `@McpValidation`으로 단순 입력 검증을 선언합니다.
- 복잡하거나 조건부 규칙이 필요한 입력은 `tool-schemas/{categoryKey}/...-input-schema.json` 리소스를 사용합니다. - 복잡하거나 조건부 규칙이 필요한 입력은 `tool-schemas/{categoryKey}/...-input-schema.json` 리소스를 사용합니다.
- 응답 Schema가 필요한 경우 Response DTO에 `@McpOutputSchema`를 선언하고, 복잡한 경우 Output Schema 리소스로 명시합니다. - 응답 `@McpOutputSchema`로 간단한 설명을 제공하고, 복잡한 응답은 Output Schema 리소스로 명시합니다.
- `null`의 업무 의미, 조건부 필드, 배열 정렬 기준, `hasMore` 여부, 민감 정보 제외 원칙을 Schema 설명에 명확히 씁니다. - `null`의 업무 의미, 조건부 필드, 배열 정렬 기준, `hasMore` 여부, 민감 정보 제외 원칙을 Schema 설명에 명확히 씁니다.
### Schema 우선순위 ### Schema 우선순위
1. `inputSchemaResource` 또는 `outputSchemaResource`가 지정된 경우: 해당 JSON Schema를 사용합니다. 1. `inputSchemaResource` 또는 `outputSchemaResource`가 지정된 경우: 해당 JSON Schema를 사용합니다.
2. 리소스가 없으면: DTO`@Schema` 정보와 공통 Generator로 생성합니다. 응답 Schema는 `@McpOutputSchema`가 선언된 DTO만 자동 생성합니다. 2. 리소스가 없으면: DTO 어노테이션(`@McpValidation`, `@McpOutputSchema`)과 공통 Generator로 생성합니다.
## 8. Manifest와 Registry ## 8. Manifest와 Registry
@@ -256,14 +208,12 @@ Gateway는 Registry 등록 및 Heartbeat 정보를 이용해 Tool을 라우팅
## 9. MCI/EAI 연계 ## 9. MCI/EAI 연계
공통 MCI/EAI 연계 기능과 Glow 기본 설정은 `dat-was-lib`에서 제공합니다. Tool Pod별 애플리케이션 프로필은 각 Pod의 `application*.yml`에서 관리합니다. 공통 MCI/EAI 연계 기능과 Glow 기본 설정은 `dap-was-lib`에서 제공합니다. Tool Pod별 애플리케이션 프로필은 각 Pod의 `application*.yml`에서 관리합니다.
```text ```text
dat-was-lib/src/main/resources/glow/application-glow*.yml dap-was-lib/src/main/resources/glow/application-glow*.yml
dat-was-sal/src/main/resources/application*.yml dap-was-sms/src/main/resources/application*.yml
dat-was-cus/src/main/resources/application*.yml dap-was-oth/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`이면 호출 대상은 다음과 같습니다. MCI 호출 주소는 일반적으로 `host + uri`로 구성합니다. 예를 들어 `host=https://dev-ichmci.shinhanlife.co.kr`, `uri=/ntl_mci/clc_rcv`이면 호출 대상은 다음과 같습니다.
@@ -276,11 +226,9 @@ https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv
## 10. 품질 검증 ## 10. 품질 검증
```powershell ```powershell
.\gradlew.bat :dat-was-lib:test .\gradlew.bat :dap-was-lib:test
.\gradlew.bat :dat-was-cus:compileJava .\gradlew.bat :dap-was-oth:compileJava
.\gradlew.bat :dat-was-sal:compileJava .\gradlew.bat :dap-was-sms:compileJava
.\gradlew.bat :dat-was-pro:compileJava
.\gradlew.bat :dat-was-sys:compileJava
.\gradlew.bat validateMcpToolNames .\gradlew.bat validateMcpToolNames
``` ```
@@ -305,31 +253,29 @@ https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv
| 기능 | 위치 | | 기능 | 위치 |
|---|---| |---|---|
| Tool 공통 Controller | `dat-was-lib/src/main/java/io/shinhanlife/dat/mcc/presentation` | | Tool 공통 Controller | `dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation` |
| Tool 어노테이션 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/annotation` | | Tool 어노테이션 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation` |
| JSON Schema/검증 | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/schema`, `.../validation` | | JSON Schema/검증 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/schema`, `.../validation` |
| Manifest | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/manifest` | | Manifest | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest` |
| Tool Scaffold | `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/ToolScaffolder.java` | | Tool Scaffold | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java` |
| Gateway 라우터 | `dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/McpRouterController.java` | | Gateway 라우터 | `dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java` |
| Customer Tool | `dat-was-cus/src/main/java` | | OTH Tool | `dap-was-oth/src/main/java` |
| Sales Tool | `dat-was-sal/src/main/java` | | SMS Tool | `dap-was-sms/src/main/java` |
| Process Tool | `dat-was-pro/src/main/java` |
| System Tool | `dat-was-sys/src/main/java` |
## 13. 모듈명 전환 검증 ## 13. 모듈명 전환 검증
Tool 관련 공통 기능은 `dat-was-*` 모듈명만 기준으로 동작합니다. Tool 관련 공통 기능은 `dap-was-*` 모듈명만 기준으로 동작합니다.
- `validateMcpToolNames``dat-was-*` Tool Pod를 탐색하여 이름 규칙과 전역 중복을 검사합니다. - `validateMcpToolNames``dap-was-*` Tool Pod를 탐색하여 이름 규칙과 전역 중복을 검사합니다.
- Tool Scaffold는 Pod 이름을 Tool 함수명에 포함하지 않습니다. 함수명은 `도메인_비즈니스_행위` 형식입니다. 예: `cmm_notification_send` - Tool Scaffold는 Pod 이름을 Tool 함수명에 포함하지 않습니다. 함수명은 `도메인_비즈니스_행위` 형식입니다. 예: `cmm_notification_send`
- Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dat-was-*` 명칭으로 통일되어 있습니다. - Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dap-was-*` 명칭으로 통일되어 있습니다.
- Tool Source Update 기능은 `dat-was-*` 아래의 `*UseCase.java`를 검색합니다. - Tool Source Update 기능은 `dap-was-*` 아래의 `*UseCase.java`를 검색합니다.
## 14. BC-DAB-STD-003 Tool Schema V17 적용 ## 14. BC-DAB-STD-003 Tool Schema V17 적용
각 Tool의 표준 명세는 Tool Pod별 다음 경로에서 관리합니다. 각 Tool의 표준 명세는 Tool Pod별 다음 경로에서 관리합니다.
```text ```text
dat-was-{pod}/src/main/resources/tool-definitions/{categoryKey}/{toolName}.yml dap-was-{pod}/src/main/resources/tool-definitions/{categoryKey}/{toolName}.yml
``` ```
Tool 이름은 Pod 정보를 포함하지 않는 `도메인_서비스_행위` 형태의 영문 소문자 snake_case를 사용하며, Tool 이름은 Pod 정보를 포함하지 않는 `도메인_서비스_행위` 형태의 영문 소문자 snake_case를 사용하며,
@@ -346,13 +292,11 @@ destructive, idempotent), `parameters_schema`입니다. 입력 Schema는 루트
입력 Schema 우선순위는 `inputSchemaResource` → V17 `parameters_schema` → DTO 자동 생성이고, 출력은 입력 Schema 우선순위는 `inputSchemaResource` → V17 `parameters_schema` → DTO 자동 생성이고, 출력은
`outputSchemaResource` → 명시 Output Schema → `@McpOutputSchema` 기반 생성입니다. Output Schema를 `outputSchemaResource` → 명시 Output Schema → `@McpOutputSchema` 기반 생성입니다. Output Schema를
명시한 Tool만 최종 응답 검증을 수행합니다. 현재 Scaffold는 Tool Definition V17 YAML과 응답 Mock JSON을 명시한 Tool만 최종 응답 검증을 수행합니다.
자동 생성하지 않고, 생성한 Java Tool의 `@GrowToolHint`를 기준으로 메타데이터를 제공합니다.
Scaffold의 Pod Module 생성은 `local`, `dev`, `test`, `prod` 프로필 파일과 Scaffold 화면의 `Tool Schema V17 Metadata` 영역에서는 기능 설명, 사용/비사용 조건, 입출력 제한,
`src/main/resources/tool-service-manifest.yml`을 만듭니다. 이 파일은 라우팅용 설정이며 `/tool-manifest` 표시 설명, 예시 질의, 태그와 소유 조직을 입력합니다. Java 소스와 함께 V17 YAML이 생성되며, 기본값은
API 응답을 대체하지 않습니다. AI 초안은 한국어 YAML로 생성하며, 선택한 Target Module 목록에서 자기 배포 전에 업무 담당자가 실제 의미에 맞게 검토해야 합니다.
자신을 제외해 `confusable-servers`에 반영합니다. Tool Function의 기본 Protocol은 MCI(Legacy)입니다.
```powershell ```powershell
.\gradlew.bat validateMcpToolNames validateToolSchemaV17 .\gradlew.bat validateMcpToolNames validateToolSchemaV17

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,107 @@
// 루트 Gradle 설정: 모든 Gateway/Tool Pod 모듈이 공유하는 빌드 기준을 정의합니다.
plugins { plugins {
// Java 컴파일, 테스트, JAR 생성 기능을 제공합니다.
id 'java' id 'java'
id 'org.springframework.boot' version '3.5.11'
id 'io.spring.dependency-management' version '1.1.7' // 하위 실행 모듈에서 bootRun/bootJar를 사용하기 위한 Spring Boot 플러그인입니다.
// 루트 프로젝트에는 적용하지 않으므로 apply false를 사용합니다.
id 'org.springframework.boot' version '3.5.11' apply false
// Spring Boot / Spring AI BOM에 정의된 라이브러리 버전을 일관되게 적용합니다.
id 'io.spring.dependency-management' version '1.1.6' apply false
} }
// 모든 모듈이 공유하는 Maven 식별자입니다.
// ============================================================================
// 전체 프로젝트 공통 Maven 좌표
// ============================================================================
allprojects { allprojects {
// 모든 모듈이 공유하는 Maven Group ID입니다.
group = 'io.shinhanlife' group = 'io.shinhanlife'
// 모든 모듈이 공유하는 프로젝트 버전입니다.
version = '0.0.1-SNAPSHOT' version = '0.0.1-SNAPSHOT'
} }
// dap-gateway, dap-was-lib, dap-was-oth, dap-was-sms에 공통 적용합니다.
subprojects { subprojects {
// 모든 하위 모듈에서 Java Plugin을 사용합니다.
apply plugin: 'java' apply plugin: 'java'
// Spring Boot BOM 기반 의존성 버전 관리를 위해 적용합니다.
apply plugin: 'io.spring.dependency-management' apply plugin: 'io.spring.dependency-management'
java { java {
toolchain { // 프로젝트 표준 Java 버전입니다.
languageVersion = JavaLanguageVersion.of(21) sourceCompatibility = '21'
}
} }
repositories { repositories {
// 현재 공개 정식 라이브러리 저장소입니다.
// -------------------------------------------------------------------- // 폐쇄망 적용 시 사내 Nexus Proxy URL로 교체합니다.
// 외부 개발 환경
// --------------------------------------------------------------------
//
// 현재 인터넷이 가능한 환경에서는 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() mavenCentral()
} }
// ========================================================================
// Dependency Version Management
// ========================================================================
dependencyManagement { dependencyManagement {
imports { imports {
// Spring Boot 3.5.11과 호환되는 Spring/Jackson/Tomcat 등의 버전을 관리합니다.
// Spring Boot 3.5.11이 검증한
// Spring Framework / Jackson / Tomcat / Logback /
// JUnit 등 주요 라이브러리 버전을 공통 관리합니다.
// 하위 모듈에서는 Spring Boot가 관리하는 라이브러리라면
// 가급적 개별 버전을 직접 지정하지 않고 사용합니다.
mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
// Gateway에서 사용하는 Spring AI 의존성 버전 관리 // Spring AI, MCP, OpenAI 연동 라이브러리의 호환 버전을 1.1.8로 고정합니다.
mavenBom 'org.springframework.ai:spring-ai-bom:1.1.8' mavenBom 'org.springframework.ai:spring-ai-bom:1.1.8'
// MCP SDK 버전 관리
mavenBom 'io.modelcontextprotocol.sdk:mcp-bom:2.0.0'
} }
} }
// ========================================================================
// Common Dependencies
// ========================================================================
dependencies { dependencies {
// Lombok은 컴파일 시 getter/builder 등 반복 코드를 생성하며 실행 JAR에는 포함하지 않습니다.
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
testCompileOnly 'org.projectlombok:lombok:1.18.32'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.32'
// ==================================================================== // Tool DTO와 MCI 요청/응답 객체 간 Converter 구현체를 컴파일 시 자동 생성합니다.
// Lombok 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'
// Lombok은 컴파일 시 Getter / Setter / Builder 등의 // 모든 모듈의 JUnit 5 기반 테스트 공통 의존성입니다.
// 반복 코드를 생성합니다.
//
// 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 등
// 공통 테스트 환경을 제공합니다.
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Gradle / IDE 환경에서 JUnit Platform 기반 테스트 실행을 지원합니다.
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
} }
tasks.withType(JavaCompile) {
// ======================================================================== // 리플렉션/MCP Schema 생성 시 메서드 파라미터명을 사용할 수 있도록 보존합니다.
// Java Compile Options
// ========================================================================
tasks.withType(JavaCompile).configureEach {
// Reflection 및 MCP Schema 생성 시
// Java 메서드의 실제 파라미터명을 사용할 수 있도록
// 컴파일 결과에 파라미터 이름을 보존합니다.
options.compilerArgs << '-parameters' options.compilerArgs << '-parameters'
// MapStruct가 생성하는 Converter 구현 클래스를 // MapStruct 구현체를 Spring Bean으로 생성해 생성자 주입으로 사용할 수 있게 합니다.
// Spring Bean(@Component)으로 생성합니다.
//
// 이를 통해 생성자 주입 등의 방식으로 사용할 수 있습니다.
options.compilerArgs << '-Amapstruct.defaultComponentModel=spring' options.compilerArgs << '-Amapstruct.defaultComponentModel=spring'
} }
tasks.withType(Test) {
// ======================================================================== // JUnit 5 테스트 플랫폼을 사용합니다.
// Test Configuration
// ========================================================================
tasks.withType(Test).configureEach {
// 모든 하위 모듈에서 JUnit 5 Platform을 사용합니다.
useJUnitPlatform() useJUnitPlatform()
} }
} }
task exportAllLibs(type: Copy) { // Tool 이름 중복 검사 프로그램이 포함된 공통 라이브러리 모듈입니다.
into 'all-exported-libs' def toolCoreProject = project(':dap-was-lib')
subprojects {
from(it.configurations.findByName('runtimeClasspath')) // 전체 Tool Pod의 @McpTool(name) 중복을 배포 산출물 생성 전에 차단합니다.
} tasks.register('validateMcpToolNames', JavaExec) {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE group = 'verification'
} description = 'Checks duplicate @McpTool names across all Tool modules before packaging.'
// 검사 Runner를 실행하기 전에 dap-was-lib 클래스를 먼저 컴파일합니다.
dependsOn toolCoreProject.tasks.named('classes')
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
mainClass.set('io.shinhanlife.dap.lib.validation.McpToolNameValidationRunner')
args rootProject.projectDir.absolutePath
}
tasks.register('validateToolSchemaV17', JavaExec) {
group = 'verification'
description = 'Validates BC-DAB-STD-003 V17 definitions for every @McpTool.'
dependsOn toolCoreProject.tasks.named('classes')
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
mainClass.set('io.shinhanlife.dap.lib.validation.ToolSchemaV17ValidationRunner')
args rootProject.projectDir.absolutePath
}
subprojects {
// 배포용 Spring Boot JAR 생성 전에 Tool 이름 중복 검증을 강제합니다.
tasks.matching { it.name == 'bootJar' }.configureEach {
dependsOn rootProject.tasks.named('validateMcpToolNames')
dependsOn rootProject.tasks.named('validateToolSchemaV17')
}
}

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -5,7 +5,7 @@ plugins {
dependencies { dependencies {
// Tool Registry, MCP , /Schema . // Tool Registry, MCP , /Schema .
implementation project(':dat-was-lib') implementation project(':dap-was-lib')
// Gateway REST API와 HTTP . // Gateway REST API와 HTTP .
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
@@ -13,12 +13,15 @@ dependencies {
// Excel Tool . // Excel Tool .
implementation 'org.apache.poi:poi-ooxml:5.5.1' implementation 'org.apache.poi:poi-ooxml:5.5.1'
// Tool Registry .
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Tool/Registry DB MyBatis Mapper . // Tool/Registry DB MyBatis Mapper .
implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
// Gateway MCP Server/Client는 MCP Java SDK 2.0.0 . // Gateway /mcp Endpoint를 MCP Server로 .
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
// ChatClient를 OpenAI/OpenRouter . // ChatClient를 OpenAI/OpenRouter .
implementation 'org.springframework.ai:spring-ai-starter-model-openai' implementation 'org.springframework.ai:spring-ai-starter-model-openai'
@@ -27,6 +30,8 @@ dependencies {
runtimeOnly 'com.h2database:h2' runtimeOnly 'com.h2database:h2'
// SQL JDBC . // SQL JDBC .
implementation 'p6spy:p6spy:3.9.1'
// Gateway REST API Swagger UI를 . // Gateway REST API Swagger UI를 .
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
} }

View File

@@ -1,9 +1,9 @@
package io.shinhanlife.dat.mcg; package io.shinhanlife.dap.mcg;
/** /**
* @package io.shinhanlife.dat.mcg * @package io.shinhanlife.dap.mcg
* @className DatGatewayApplication * @className DapGatewayApplication
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
@@ -21,12 +21,12 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication(scanBasePackages = {"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.dat.mcg", "io.shinhanlife.dat.lib.mcp", "io.shinhanlife.dat.lib.config"}) @ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcg", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config"})
@EnableCaching @EnableCaching
@EnableScheduling @EnableScheduling
public class DatGatewayApplication { public class DapGatewayApplication {
public static void main(String[] args) { 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 lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch; import org.springframework.util.StopWatch;
/** /**
* @package io.shinhanlife.dat.mcg.aop * @package io.shinhanlife.dap.mcg.aop
* @className GatewayLoggingAspect * @className GatewayLoggingAspect
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -27,7 +27,7 @@ import org.springframework.util.StopWatch;
public class GatewayLoggingAspect { public class GatewayLoggingAspect {
// gateway의 controller 패키지 하위의 모든 클래스/메서드 실행 작동 // gateway의 controller 패키지 하위의 모든 클래스/메서드 실행 작동
@Around("execution(* io.shinhanlife.dat.mcg.presentation..*(..))") @Around("execution(* io.shinhanlife.dap.mcg.presentation..*(..))")
public Object logGatewayExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { public Object logGatewayExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String targetMethod = joinPoint.getSignature().toShortString(); String targetMethod = joinPoint.getSignature().toShortString();
StopWatch stopWatch = new StopWatch(); 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 * @className AuditLogService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -15,8 +15,8 @@ package io.shinhanlife.dat.mcg.audit;
* *
* </pre> * </pre>
*/ */
import io.shinhanlife.dat.mcg.config.McpGatewayProperties; import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dat.mcg.security.McpRequestContext; import io.shinhanlife.dap.mcg.security.McpRequestContext;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 * @className AgentResponseBudgetProperties
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dat.mcg.config; package io.shinhanlife.dap.mcg.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -8,7 +8,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* @package io.shinhanlife.dat.mcg.config * @package io.shinhanlife.dap.mcg.config
* @className GatewayFallbackProperties * @className GatewayFallbackProperties
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className McpGatewayProperties
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -37,8 +37,8 @@ public class McpGatewayProperties {
private Boolean agentClaimsRequired; private Boolean agentClaimsRequired;
private Boolean trustedClaimsRequired; private Boolean trustedClaimsRequired;
private Boolean writeApprovalRequired; private Boolean writeApprovalRequired;
private Boolean traceEnabled; private Boolean redisTraceEnabled;
private Long traceRetentionSize; private Long redisTraceTtlSeconds;
private Long toolTimeoutMillis; private Long toolTimeoutMillis;
private Integer retryMaxAttempts; private Integer retryMaxAttempts;
private Long retryInitialBackoffMillis; private Long retryInitialBackoffMillis;
@@ -70,8 +70,8 @@ public class McpGatewayProperties {
public void setAgentClaimsRequired(Boolean agentClaimsRequired) { this.agentClaimsRequired = agentClaimsRequired; } public void setAgentClaimsRequired(Boolean agentClaimsRequired) { this.agentClaimsRequired = agentClaimsRequired; }
public void setTrustedClaimsRequired(Boolean trustedClaimsRequired) { this.trustedClaimsRequired = trustedClaimsRequired; } public void setTrustedClaimsRequired(Boolean trustedClaimsRequired) { this.trustedClaimsRequired = trustedClaimsRequired; }
public void setWriteApprovalRequired(Boolean writeApprovalRequired) { this.writeApprovalRequired = writeApprovalRequired; } public void setWriteApprovalRequired(Boolean writeApprovalRequired) { this.writeApprovalRequired = writeApprovalRequired; }
public void setTraceEnabled(Boolean traceEnabled) { this.traceEnabled = traceEnabled; } public void setRedisTraceEnabled(Boolean redisTraceEnabled) { this.redisTraceEnabled = redisTraceEnabled; }
public void setTraceRetentionSize(Long traceRetentionSize) { this.traceRetentionSize = traceRetentionSize; } public void setRedisTraceTtlSeconds(Long redisTraceTtlSeconds) { this.redisTraceTtlSeconds = redisTraceTtlSeconds; }
public void setToolTimeoutMillis(Long toolTimeoutMillis) { this.toolTimeoutMillis = toolTimeoutMillis; } public void setToolTimeoutMillis(Long toolTimeoutMillis) { this.toolTimeoutMillis = toolTimeoutMillis; }
public void setRetryMaxAttempts(Integer retryMaxAttempts) { this.retryMaxAttempts = retryMaxAttempts; } public void setRetryMaxAttempts(Integer retryMaxAttempts) { this.retryMaxAttempts = retryMaxAttempts; }
public void setRetryInitialBackoffMillis(Long retryInitialBackoffMillis) { this.retryInitialBackoffMillis = retryInitialBackoffMillis; } public void setRetryInitialBackoffMillis(Long retryInitialBackoffMillis) { this.retryInitialBackoffMillis = retryInitialBackoffMillis; }
@@ -103,8 +103,8 @@ public class McpGatewayProperties {
public boolean agentClaimsRequired() { return agentClaimsRequired == null || agentClaimsRequired; } public boolean agentClaimsRequired() { return agentClaimsRequired == null || agentClaimsRequired; }
public boolean trustedClaimsRequired() { return trustedClaimsRequired == null || trustedClaimsRequired; } public boolean trustedClaimsRequired() { return trustedClaimsRequired == null || trustedClaimsRequired; }
public boolean writeApprovalRequired() { return writeApprovalRequired == null || writeApprovalRequired; } public boolean writeApprovalRequired() { return writeApprovalRequired == null || writeApprovalRequired; }
public boolean traceEnabled() { return traceEnabled == null || traceEnabled; } public boolean redisTraceEnabled() { return redisTraceEnabled == null || redisTraceEnabled; }
public long traceRetentionSize() { return traceRetentionSize == null || traceRetentionSize < 1 ? 1000 : traceRetentionSize; } public long redisTraceTtlSeconds() { return redisTraceTtlSeconds == null || redisTraceTtlSeconds < 1 ? 3_600 : redisTraceTtlSeconds; }
public long toolTimeoutMillis() { return toolTimeoutMillis == null || toolTimeoutMillis < 1 ? 5_000 : toolTimeoutMillis; } public long toolTimeoutMillis() { return toolTimeoutMillis == null || toolTimeoutMillis < 1 ? 5_000 : toolTimeoutMillis; }
public int retryMaxAttempts() { return retryMaxAttempts == null || retryMaxAttempts < 1 ? 3 : retryMaxAttempts; } public int retryMaxAttempts() { return retryMaxAttempts == null || retryMaxAttempts < 1 ? 3 : retryMaxAttempts; }
public long retryInitialBackoffMillis() { return retryInitialBackoffMillis == null || retryInitialBackoffMillis < 1 ? 1_000 : retryInitialBackoffMillis; } 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.mcc.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,6 +1,6 @@
package io.shinhanlife.dat.mcg.document; package io.shinhanlife.dap.mcg.document;
import io.shinhanlife.dat.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
public record DocumentGenerationRequest( public record DocumentGenerationRequest(
ToolMetadata tool, ToolMetadata tool,

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dat.mcg.document; package io.shinhanlife.dap.mcg.document;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;

View File

@@ -1,8 +1,8 @@
package io.shinhanlife.dat.mcg.document; package io.shinhanlife.dap.mcg.document;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dat.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dat.mcg.document; package io.shinhanlife.dap.mcg.document;
public record GeneratedDocument(String fileName, byte[] content) { public record GeneratedDocument(String fileName, byte[] content) {
} }

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 * @className GuardrailService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -15,9 +15,9 @@ package io.shinhanlife.dat.mcg.guardrail;
* *
* </pre> * </pre>
*/ */
import io.shinhanlife.dat.mcg.resilience.FailureType; import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dat.mcg.resilience.ToolExecutionException; import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dat.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Service; 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 * @className ToolResponseGuardrailService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -15,9 +15,9 @@ package io.shinhanlife.dat.mcg.guardrail;
* *
* </pre> * </pre>
*/ */
import io.shinhanlife.dat.mcg.resilience.FailureType; import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dat.mcg.resilience.ToolExecutionException; import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dat.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -10,7 +10,7 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
* @package io.shinhanlife.dat.mcg.messaging * @package io.shinhanlife.dap.mcg.messaging
* @className KafkaProducerService * @className KafkaProducerService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className ChatController
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -15,10 +15,10 @@ package io.shinhanlife.dat.mcg.presentation;
* *
* </pre> * </pre>
*/ */
import io.shinhanlife.dat.lib.adapter.dto.JsonRpcResponse; import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dat.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcg.registry.InMemoryRegistryService; import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dat.mcg.service.ExecuteService; import io.shinhanlife.dap.mcg.service.ExecuteService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient;
@@ -41,19 +41,10 @@ public class ChatController {
private final ExecuteService executeService; private final ExecuteService executeService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final InMemoryRegistryService registryService; private final RedisRegistryService registryService;
private final ChatClient.Builder chatClientBuilder; private final ChatClient.Builder chatClientBuilder;
private final McpRouterController mcpRouterController; 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) @PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody Map<String, String> request, public SseEmitter chatStream(@RequestBody Map<String, String> request,
@RequestHeader(value = "X-Agent-Id", required = false) String agentId, @RequestHeader(value = "X-Agent-Id", required = false) String agentId,
@@ -70,18 +61,12 @@ public class ChatController {
if (toolsResponse != null && toolsResponse.getResult() instanceof Map) { if (toolsResponse != null && toolsResponse.getResult() instanceof Map) {
Map<String, Object> resultMap = (Map<String, Object>) toolsResponse.getResult(); Map<String, Object> resultMap = (Map<String, Object>) toolsResponse.getResult();
if (resultMap.containsKey("tools")) { 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<>(); Set<String> addedToolNames = new HashSet<>();
for (Map<String, Object> metaMap : allTools) { for (ToolMetadata meta : allTools) {
String name = (String) metaMap.get("name"); if (Boolean.TRUE.equals(meta.getVisible()) && meta.getName() != null) {
if (name != null) { String cleanName = meta.getName().replaceAll("[^a-zA-Z0-9_-]", "_");
String cleanName = name.replaceAll("[^a-zA-Z0-9_-]", "_");
if (!addedToolNames.contains(cleanName)) { 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)); callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
addedToolNames.add(cleanName); addedToolNames.add(cleanName);
} }
@@ -90,59 +75,27 @@ public class ChatController {
} }
} }
String selectedModel = request.getOrDefault("model", "Qwen3-Coder").trim(); String selectedModel = request.getOrDefault("model", "cohere/north-mini-code:free").trim();
if (selectedModel.isEmpty()) { if (selectedModel.isEmpty()) {
selectedModel = "Qwen3-Coder"; selectedModel = "cohere/north-mini-code:free";
} }
// LiteLLM 내부망 모델 매핑 // 1. gemini-flash-latest 선택 OpenRouter의 Google Gemma 4 모델 라우팅
if (selectedModel.equals("gemini-flash-latest")) { if (selectedModel.equals("gemini-flash-latest")) {
selectedModel = "Gemma-4-31B"; selectedModel = "google/gemma-4-31b-it:free";
log.info("[Real AI Chat] 화면의 Gemini 플래시 선택을 사내 Gemma-4-31B 모델로 라우팅 전환"); log.info("[Real AI Chat] Gemini 직접 API → OpenRouter Google Gemma 4 31B 로 라우팅 전환");
} }
ChatClient activeChatClient; // 2. OpenRouter 무료 모델 처리 (Spring AI 빌드된 ChatClient MCP 호출 완벽 지원!)
org.springframework.ai.chat.prompt.ChatOptions chatOptions; 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.")
// 2. 모델 분기 처리: 사내 모델(Qwen3/Gemma-4) vs 기존 외부 모델(OpenRouter ) .build();
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();
}
Flux<String> responseStream = activeChatClient.prompt() Flux<String> responseStream = activeChatClient.prompt()
.user(message) .user(message)
.toolCallbacks(callbacks.toArray(new ToolCallback[0])) .toolCallbacks(callbacks.toArray(new ToolCallback[0]))
.options(chatOptions) .options(org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(selectedModel).build())
.stream() .stream()
.content(); .content();

View File

@@ -0,0 +1,72 @@
package io.shinhanlife.dap.mcg.presentation;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientResponseException;
/**
* Gateway 화면에서 들어온 DTO 엑셀 요청을 실제 DTO 클래스가 있는 Tool Pod로 전달한다.
* 브라우저가 동일 출처(8081)만 호출하도록 하여 CORS와 배포 주소 차이를 숨긴다.
*/
@RestController
public class DtoDownloadProxyController {
private static final MediaType XLSX_MEDIA_TYPE = MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
private final RestClient restClient;
private final String toolPodUrl;
public DtoDownloadProxyController(
RestClient.Builder restClientBuilder,
@Value("${mcp.gateway.fallback.default-url:http://localhost:8084}") String toolPodUrl) {
this.restClient = restClientBuilder.build();
this.toolPodUrl = toolPodUrl.replaceAll("/+$", "");
}
@GetMapping("/dto-download/options")
public List<String> options() {
// 셀렉트박스에 표시할 DTO 이름 목록을 Tool Pod에서 조회한다.
List<String> options = restClient.get()
.uri(toolPodUrl + "/dto-download/options")
.retrieve()
.body(new ParameterizedTypeReference<>() {});
return options == null ? List.of() : options;
}
@GetMapping("/dto-download/{dtoName}")
public ResponseEntity<byte[]> download(@PathVariable String dtoName) {
byte[] workbook;
try {
// 생성된 엑셀 바이트를 그대로 브라우저에 전달한다.
workbook = restClient.get()
.uri(toolPodUrl + "/dto-download/{dtoName}", dtoName)
.retrieve()
.body(byte[].class);
} catch (RestClientResponseException error) {
// 형식 불일치 등의 상태 코드와 오류 메시지도 변경 없이 전달한다.
return ResponseEntity.status(error.getStatusCode())
.contentType(error.getResponseHeaders() != null
&& error.getResponseHeaders().getContentType() != null
? error.getResponseHeaders().getContentType() : MediaType.TEXT_PLAIN)
.body(error.getResponseBodyAsByteArray());
}
String fileName = dtoName + ".xlsx";
return ResponseEntity.ok()
.contentType(XLSX_MEDIA_TYPE)
.contentLength(workbook == null ? 0 : workbook.length)
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.attachment().filename(fileName).build().toString())
.body(workbook == null ? new byte[0] : workbook);
}
}

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 lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
/** /**
* @package io.shinhanlife.dat.mcg.presentation * @package io.shinhanlife.dap.mcg.presentation
* @className KillSwitchController * @className KillSwitchController
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -36,9 +36,9 @@ public class KillSwitchController {
*/ */
@PostMapping("/{type}/{target}") @PostMapping("/{type}/{target}")
public ResponseEntity<?> toggleKillSwitch( public ResponseEntity<?> toggleKillSwitch(
@PathVariable("type") String type, @PathVariable String type,
@PathVariable("target") String target, @PathVariable String target,
@RequestParam("state") boolean state) { @RequestParam boolean state) {
killSwitchService.toggleKillSwitch(type, target, state); killSwitchService.toggleKillSwitch(type, target, state);
@@ -55,8 +55,8 @@ public class KillSwitchController {
*/ */
@DeleteMapping("/{type}/{target}") @DeleteMapping("/{type}/{target}")
public ResponseEntity<?> removeKillSwitch( public ResponseEntity<?> removeKillSwitch(
@PathVariable("type") String type, @PathVariable String type,
@PathVariable("target") String target) { @PathVariable String target) {
killSwitchService.removeKillSwitch(type, 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 * @className McpRouterController
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -16,15 +16,14 @@ package io.shinhanlife.dat.mcg.presentation;
* </pre> * </pre>
*/ */
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dat.lib.adapter.dto.JsonRpcRequest; import io.shinhanlife.dap.lib.adapter.dto.JsonRpcRequest;
import io.shinhanlife.dat.lib.adapter.dto.JsonRpcResponse; import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dat.lib.adapter.dto.Params; import io.shinhanlife.dap.lib.adapter.dto.Params;
import io.shinhanlife.dat.mcg.config.GatewayFallbackProperties; import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dat.mcg.config.McpGatewayProperties; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dat.mcg.registry.InMemoryRegistryService; import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.shinhanlife.dat.mcg.service.ExecuteService; import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dat.lib.mcp.security.SecurityProperties;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -50,20 +49,19 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory;
@Tag(name = "MCP Router API", description = "AI Agent의 요청을 받아 Adapter 시스템으로 라우팅하는 게이트웨이 API") @Tag(name = "MCP Router API", description = "AI Agent의 요청을 받아 Adapter 시스템으로 라우팅하는 게이트웨이 API")
public class McpRouterController { public class McpRouterController {
private final InMemoryRegistryService registryService; private final RedisRegistryService redisRegistryService;
private final ExecuteService executeService; private final ExecuteService executeService;
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final GatewayFallbackProperties gatewayFallbackProperties; private final GatewayFallbackProperties gatewayFallbackProperties;
private final RestClient restClient; private final RestClient restClient;
public McpRouterController(InMemoryRegistryService registryService, public McpRouterController(RedisRegistryService redisRegistryService,
ExecuteService executeService, ExecuteService executeService,
SecurityProperties securityProperties, SecurityProperties securityProperties,
ObjectMapper objectMapper, ObjectMapper objectMapper,
GatewayFallbackProperties gatewayFallbackProperties, GatewayFallbackProperties gatewayFallbackProperties) {
McpGatewayProperties mcpGatewayProperties) { this.redisRegistryService = redisRegistryService;
this.registryService = registryService;
this.executeService = executeService; this.executeService = executeService;
this.securityProperties = securityProperties; this.securityProperties = securityProperties;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
@@ -74,15 +72,7 @@ public class McpRouterController {
factory.setConnectTimeout(1000); // 연결 시도 타임아웃 1초 factory.setConnectTimeout(1000); // 연결 시도 타임아웃 1초
factory.setReadTimeout(1000); // 응답 대기 타임아웃 1초 factory.setReadTimeout(1000); // 응답 대기 타임아웃 1초
String apiKey = mcpGatewayProperties.toolServerApiKey(); this.restClient = RestClient.builder().requestFactory(factory).build();
if (apiKey == null || apiKey.isEmpty()) {
apiKey = "tool-server-key";
}
this.restClient = RestClient.builder()
.requestFactory(factory)
.defaultHeader("X-Tool-Server-API-Key", apiKey)
.build();
} }
@PostMapping("/tools/call") @PostMapping("/tools/call")
@@ -125,12 +115,12 @@ public class McpRouterController {
private List<ToolMetadata> fetchAllActiveTools() { private List<ToolMetadata> fetchAllActiveTools() {
List<ToolMetadata> activeTools = new ArrayList<>(); List<ToolMetadata> activeTools = new ArrayList<>();
try { try {
activeTools.addAll(registryService.getAllTools() activeTools.addAll(redisRegistryService.getAllTools()
.stream() .stream()
.filter(ToolMetadata::getVisible) .filter(ToolMetadata::getVisible)
.collect(Collectors.toList())); .collect(Collectors.toList()));
} catch (Exception exception) { } catch (org.springframework.data.redis.RedisConnectionFailureException exception) {
log.warn("Registry is unavailable. Fetching tools from configured fallback Tool Pods instead."); log.warn("Redis is unavailable. Fetching tools from configured fallback Tool Pods instead.");
} }
Set<String> knownTools = activeTools.stream() Set<String> knownTools = activeTools.stream()
@@ -151,10 +141,9 @@ public class McpRouterController {
if (localTools != null) { if (localTools != null) {
for (ToolMetadata t : localTools) { 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); activeTools.add(t);
knownTools.add(t.getUid()); knownTools.add(t.getUid());
registryService.saveTool(t);
} }
} }
} }
@@ -165,11 +154,6 @@ public class McpRouterController {
return activeTools; return activeTools;
} }
/** Report 화면처럼 내부 메타데이터 전체가 필요한 Gateway 컴포넌트에 활성 Tool 목록을 제공합니다. */
List<ToolMetadata> activeToolsForReport() {
return fetchAllActiveTools();
}
@GetMapping("/tools/list") @GetMapping("/tools/list")
public ResponseEntity<JsonRpcResponse> listTools( public ResponseEntity<JsonRpcResponse> listTools(
@RequestParam(value = "categoryKey", required = false) String categoryKey) { @RequestParam(value = "categoryKey", required = false) String categoryKey) {
@@ -182,40 +166,9 @@ public class McpRouterController {
.collect(Collectors.toList()); .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(); JsonRpcResponse response = new JsonRpcResponse();
response.setId(UUID.randomUUID().toString()); response.setId(UUID.randomUUID().toString());
response.setResult(Map.of("tools", mappedTools)); response.setResult(Map.of("tools", activeTools));
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@@ -287,15 +240,23 @@ public class McpRouterController {
@PostMapping("/registry/register") @PostMapping("/registry/register")
public ResponseEntity<String> registerTool(@RequestBody ToolMetadata meta) { public ResponseEntity<String> registerTool(@RequestBody ToolMetadata meta) {
registryService.saveTool(meta); redisRegistryService.saveTool(meta);
return ResponseEntity.ok("Registered"); return ResponseEntity.ok("Registered");
} }
@PostMapping("/registry/deregister") @PostMapping("/registry/deregister")
public ResponseEntity<String> deregisterTool(@RequestBody String uid) { public ResponseEntity<String> deregisterTool(@RequestBody String uid) {
registryService.removeTool(uid); redisRegistryService.removeTool(uid);
return ResponseEntity.ok("Deregistered"); 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,417 @@
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 com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/scaffold")
public class ScaffoldingController {
private static final Set<String> SUPPORTED_FIELD_TYPES = Set.of(
"String", "Integer", "Long", "Double", "Boolean", "BigDecimal", "Enum", "List");
private static final Set<String> SUPPORTED_AI_MODELS = Set.of(
"inclusionai/ling-3.0-flash:free",
"openai/gpt-oss-20b:free",
"google/gemma-4-31b-it:free",
"nvidia/nemotron-3-nano-30b-a3b:free",
"cohere/north-mini-code:free");
private final ChatClient.Builder chatClientBuilder;
private final ObjectMapper objectMapper;
public ScaffoldingController(ChatClient.Builder chatClientBuilder, ObjectMapper objectMapper) {
this.chatClientBuilder = chatClientBuilder;
this.objectMapper = objectMapper;
}
@PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) {
try {
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
if (!moduleName.startsWith("dap-was-")) moduleName = "dap-was-" + moduleName;
String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("dap-was-", "").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 title = req.get("title");
if (title == null || title.isBlank()) title = baseName;
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");
String httpApiName = req.get("httpApiName");
String inputSchemaResource = req.get("inputSchemaResource");
String outputSchemaResource = req.get("outputSchemaResource");
List<ToolScaffolder.FieldDefinition> inputFields = parseFields(req.get("inputFields"));
List<ToolScaffolder.FieldDefinition> outputFields = parseFields(req.get("outputFields"));
if (inputFields.isEmpty()) {
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false));
}
ToolScaffolder.ToolDefinitionOptions definitionOptions = new ToolScaffolder.ToolDefinitionOptions(
req.get("functionDescription"),
req.get("whenToUse"),
req.get("whenNotToUse"),
req.get("ioLimits"),
req.get("displayDescription"),
parseDelimited(req.get("exampleQueries")),
parseDelimited(req.get("tags")),
req.get("ownerOrg"));
return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType,
moduleName, author, date, register, clientSystemCode, inputSchemaResource,
outputSchemaResource, inputFields, outputFields, httpApiName, definitionOptions);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool-group")
public String scaffoldToolGroup(@RequestBody ToolGroupRequest request) {
try {
if (request == null || request.useCaseName() == null
|| !request.useCaseName().trim().matches("^[A-Z][A-Za-z0-9]*$")) {
throw new IllegalArgumentException("UseCase name must be PascalCase.");
}
String moduleName = request.moduleName() == null || request.moduleName().isBlank()
? "dap-was-oth" : request.moduleName().trim();
String author = request.author() == null || request.author().isBlank()
? System.getProperty("user.name") : request.author().trim();
String date = request.date() == null || request.date().isBlank()
? LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")) : request.date().trim();
return ToolScaffolder.scaffoldUseCase(request.useCaseName().trim(), moduleName, author, date,
request.tools() == null ? List.of() : request.tools());
} catch (Exception e) {
return "Error: " + safeMessage(e);
}
}
@GetMapping("/usecases")
public List<String> listUseCases(@RequestParam String moduleName, @RequestParam String categoryKey) {
if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) {
throw new IllegalArgumentException("Invalid target module.");
}
if (categoryKey == null || !categoryKey.matches("^[a-z0-9]{3}$")) {
throw new IllegalArgumentException("Invalid domain category.");
}
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir");
File useCaseDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz/"
+ categoryKey + "/usecase");
File[] files = useCaseDir.listFiles(file -> file.isFile() && file.getName().endsWith("UseCase.java"));
if (files == null) return List.of();
return Arrays.stream(files).map(File::getName)
.map(name -> name.substring(0, name.length() - ".java".length()))
.sorted().toList();
}
@PostMapping("/field-draft")
public ResponseEntity<?> generateFieldDraft(@RequestBody Map<String, String> req) {
String description = req.getOrDefault("description", "").trim();
String target = req.getOrDefault("target", "inputFields");
if (description.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "설명을 입력해주세요."));
}
if (!"inputFields".equals(target) && !"outputFields".equals(target)) {
return ResponseEntity.badRequest().body(Map.of("error", "지원하지 않는 필드 대상입니다."));
}
try {
String targetLabel = "inputFields".equals(target) ? "INPUT request DTO" : "OUTPUT response DTO";
String prompt = """
Generate Java DTO fields for an MCP tool.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"fields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
Allowed type values: String, Integer, Long, Double, Boolean, BigDecimal, Enum, List.
Enum fields must include enumValues. List fields must include itemType; use Object plus itemFields for object lists.
Generate fields only for the requested target: %s.
For OUTPUT fields, include resultCode and resultMessage when appropriate.
Keep field names valid Java camelCase identifiers. Generate at most 10 fields.
User description: %s
""".formatted(targetLabel, description);
String response = generateAiContent(prompt, req.get("model"));
FieldDraft draft = objectMapper.readValue(stripCodeFence(response), FieldDraft.class);
List<ToolScaffolder.FieldDefinition> fields = validateFields(draft.fields());
return ResponseEntity.ok(Map.of("fields", fields));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", "AI 초안 생성 실패: " + safeMessage(e)));
}
}
@PostMapping("/tool-draft")
public ResponseEntity<?> generateToolDraft(@RequestBody Map<String, String> req) {
String description = req.getOrDefault("description", "").trim();
if (description.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "Tool 설명을 입력해주세요."));
}
try {
String prompt = """
Generate an MCP Tool scaffold from the user request.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
categoryKey must be exactly three lowercase letters or digits.
routingType must be either HTTP or MCI. httpApiName can contain only letters, digits, hyphens, and underscores.
Write every V17 metadata field for its distinct purpose; do not copy the same sentence into all fields.
Generate 3 to 10 realistic exampleQueries and concise search tags. Use MCP_TOOL for ownerOrg unless the user names an owner.
Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal, Enum, List. Enum must include enumValues; List must include itemType and object lists include itemFields.
Keep all field names valid Java camelCase identifiers. Generate at most 10 fields per list.
Do not generate interfaceId or clientSystemCode; those must come from a real integration contract.
User request: %s
""".formatted(description);
String response = generateAiContent(prompt, req.get("model"));
ToolDraft draft = objectMapper.readValue(stripCodeFence(response), ToolDraft.class);
ToolDraft validatedDraft = validateToolDraft(draft);
return ResponseEntity.ok(validatedDraft);
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", "AI Tool 초안 생성 실패: " + safeMessage(e)));
}
}
@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-was-") && !f.getName().equals("dap-was-lib"));
if (files == null || files.length == 0) {
return List.of("dap-was-oth", "dap-was-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-was-hr", "dap-was-sms");
}
}
private List<ToolScaffolder.FieldDefinition> parseFields(String source) throws Exception {
if (source == null || source.isBlank()) {
return List.of();
}
return objectMapper.readValue(source, new TypeReference<List<ToolScaffolder.FieldDefinition>>() { });
}
private List<String> parseDelimited(String source) {
if (source == null || source.isBlank()) {
return List.of();
}
return Arrays.stream(source.split("[\\r\\n,]+"))
.map(String::trim)
.filter(value -> !value.isBlank())
.distinct()
.toList();
}
private List<ToolScaffolder.FieldDefinition> validateFields(List<ToolScaffolder.FieldDefinition> source) {
if (source == null || source.isEmpty()) {
throw new IllegalArgumentException("AI가 필드를 생성하지 않았습니다.");
}
Set<String> names = new LinkedHashSet<>();
return source.stream()
.limit(10)
.filter(field -> field != null && field.name() != null && !field.name().isBlank())
.map(field -> new ToolScaffolder.FieldDefinition(
field.name().trim(),
field.type() == null ? "String" : field.type().trim(),
field.description() == null ? "" : field.description().trim(),
field.example() == null ? "" : field.example().trim(),
field.required(),
field.enumValues() == null ? List.of() : field.enumValues(),
field.itemType(),
field.itemFields() == null ? List.of() : field.itemFields()))
.peek(field -> {
if (!field.name().matches("^[A-Za-z_$][A-Za-z0-9_$]*$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 필드명을 생성했습니다: " + field.name());
}
if (!SUPPORTED_FIELD_TYPES.contains(field.type())) {
throw new IllegalArgumentException("AI가 지원하지 않는 Type을 생성했습니다: " + field.type());
}
validateStructuredField(field);
if (!names.add(field.name())) {
throw new IllegalArgumentException("AI가 중복 필드명을 생성했습니다: " + field.name());
}
})
.toList();
}
private void validateStructuredField(ToolScaffolder.FieldDefinition field) {
if ("Enum".equals(field.type()) && field.enumValues().isEmpty()) {
throw new IllegalArgumentException("Enum field needs enumValues: " + field.name());
}
if ("List".equals(field.type()) && (field.itemType() == null || field.itemType().isBlank())) {
throw new IllegalArgumentException("List field needs itemType: " + field.name());
}
if ("List".equals(field.type()) && "Object".equals(field.itemType()) && field.itemFields().isEmpty()) {
throw new IllegalArgumentException("Object List field needs itemFields: " + field.name());
}
}
private ToolDraft validateToolDraft(ToolDraft draft) {
if (draft == null || draft.baseName() == null || !draft.baseName().trim().matches("^[A-Z][A-Za-z0-9]*$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 Base Name을 생성했습니다.");
}
String categoryKey = draft.categoryKey() == null ? "" : draft.categoryKey().trim().toLowerCase(Locale.ROOT);
if (!categoryKey.matches("^[a-z0-9]{3}$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 Category Key를 생성했습니다.");
}
String routingType = draft.routingType() == null ? "" : draft.routingType().trim().toUpperCase(Locale.ROOT);
if (!Set.of("HTTP", "MCI").contains(routingType)) {
throw new IllegalArgumentException("AI가 지원하지 않는 Protocol을 생성했습니다.");
}
String httpApiName = draft.httpApiName() == null ? "http-api" : draft.httpApiName().trim();
if (!httpApiName.matches("^[A-Za-z0-9_-]+$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 HTTP API Name을 생성했습니다.");
}
String title = draft.title() == null ? "" : draft.title().trim();
String description = draft.description() == null ? "" : draft.description().trim();
if (title.isBlank() || description.isBlank()) {
throw new IllegalArgumentException("AI가 Tool 제목 또는 설명을 생성하지 않았습니다.");
}
String functionDescription = textOrDefault(draft.functionDescription(), description);
String displayDescription = textOrDefault(draft.displayDescription(), title);
String whenToUse = textOrDefault(draft.whenToUse(), description + " 요청을 처리할 때 사용한다.");
String whenNotToUse = textOrDefault(draft.whenNotToUse(), "필수 입력값이 없거나 다른 업무 요청에는 사용하지 않는다.");
String ioLimits = textOrDefault(draft.ioLimits(), "정의된 입력 필드만 허용하며 정의된 응답 DTO 범위만 반환한다.");
List<String> exampleQueries = normalizedDraftList(draft.exampleQueries(), List.of(
title + " 해줘", title + " 정보를 알려줘", title + " 결과를 확인해줘"));
List<String> tags = normalizedDraftList(draft.tags(), List.of(categoryKey));
String ownerOrg = textOrDefault(draft.ownerOrg(), "MCP_TOOL");
return new ToolDraft(draft.baseName().trim(), title, description, categoryKey, routingType, httpApiName,
functionDescription, displayDescription, whenToUse, whenNotToUse, ioLimits,
exampleQueries, tags, ownerOrg,
validateFields(draft.inputFields()), validateFields(draft.outputFields()));
}
private String textOrDefault(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.trim();
}
private List<String> normalizedDraftList(List<String> values, List<String> fallback) {
if (values == null) {
return fallback;
}
List<String> normalized = values.stream()
.filter(value -> value != null && !value.isBlank())
.map(String::trim)
.distinct()
.toList();
return normalized.isEmpty() ? fallback : normalized;
}
private String generateAiContent(String prompt, String requestedModel) {
return chatClientBuilder.build().prompt()
.user(prompt)
.options(org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(resolveModel(requestedModel)).build())
.call()
.content();
}
private String resolveModel(String requestedModel) {
String model = requestedModel == null || requestedModel.isBlank()
? "cohere/north-mini-code:free" : requestedModel.trim();
if (!SUPPORTED_AI_MODELS.contains(model)) {
throw new IllegalArgumentException("지원하지 않는 AI 모델입니다.");
}
return model;
}
private String stripCodeFence(String response) {
if (response == null) {
throw new IllegalArgumentException("AI 응답이 비어 있습니다.");
}
return response.trim().replaceFirst("^```(?:json)?\\s*", "").replaceFirst("\\s*```$", "").trim();
}
private String safeMessage(Exception e) {
return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
}
private record FieldDraft(List<ToolScaffolder.FieldDefinition> fields) {
}
private record ToolDraft(String baseName, String title, String description, String categoryKey, String routingType,
String httpApiName, String functionDescription, String displayDescription,
String whenToUse, String whenNotToUse, String ioLimits,
List<String> exampleQueries, List<String> tags, String ownerOrg,
List<ToolScaffolder.FieldDefinition> inputFields,
List<ToolScaffolder.FieldDefinition> outputFields) {
}
private record ToolGroupRequest(String useCaseName, String moduleName, String author, String date,
List<ToolScaffolder.ToolMethodDefinition> tools) {
}
}

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 * @className McpMonitorEventService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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.mcc.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.mcc.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 * @className CircuitBreaker
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className CircuitBreakerMonitorController
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className CircuitBreakerService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -15,7 +15,7 @@ package io.shinhanlife.dat.mcg.resilience;
* *
* </pre> * </pre>
*/ */
import io.shinhanlife.dat.mcg.config.McpGatewayProperties; import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Clock; 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 * @className CircuitBreakerSnapshot
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className CircuitBreakerState
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className FailureType
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className RetryPolicy
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className ToolExecutionException
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className McpRequestContext
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className McpRequestContextResolver
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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 * @className ToolAuthorizationService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -15,11 +15,11 @@ package io.shinhanlife.dat.mcg.security;
* *
* </pre> * </pre>
*/ */
import io.shinhanlife.dat.mcg.config.McpGatewayProperties; import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dat.mcg.resilience.FailureType; import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dat.mcg.resilience.ToolExecutionException; import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dat.lib.dto.OperationType; import io.shinhanlife.dap.lib.dto.OperationType;
import io.shinhanlife.dat.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Set; 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 * @className ExecuteService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -17,20 +17,20 @@ package io.shinhanlife.dat.mcg.service;
*/ */
import java.util.HashMap; import java.util.HashMap;
import io.shinhanlife.dat.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcg.resilience.FailureType; import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dat.mcg.resilience.RetryPolicy; import io.shinhanlife.dap.mcg.resilience.RetryPolicy;
import io.shinhanlife.dat.mcg.resilience.ToolExecutionException; import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dat.lib.dto.OperationType; import io.shinhanlife.dap.lib.dto.OperationType;
import io.shinhanlife.dat.mcg.config.McpGatewayProperties; import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dat.mcg.guardrail.GuardrailService; import io.shinhanlife.dap.mcg.guardrail.GuardrailService;
import io.shinhanlife.dat.mcg.security.McpRequestContext; import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dat.mcg.security.McpRequestContextResolver; import io.shinhanlife.dap.mcg.security.McpRequestContextResolver;
import io.shinhanlife.dat.mcg.audit.AuditLogService; import io.shinhanlife.dap.mcg.audit.AuditLogService;
import io.shinhanlife.dat.mcg.resilience.CircuitBreaker; import io.shinhanlife.dap.mcg.resilience.CircuitBreaker;
import io.shinhanlife.dat.mcg.resilience.CircuitBreakerService; import io.shinhanlife.dap.mcg.resilience.CircuitBreakerService;
import io.shinhanlife.dat.mcg.security.ToolAuthorizationService; import io.shinhanlife.dap.mcg.security.ToolAuthorizationService;
import io.shinhanlife.dat.mcg.trace.InMemoryToolTraceService; import io.shinhanlife.dap.mcg.redis.RedisToolTraceService;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import java.util.Map; 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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import io.shinhanlife.dat.mcg.tool.large.LargeToolResponseService; import io.shinhanlife.dap.mcg.tool.large.LargeToolResponseService;
import io.shinhanlife.dat.mcg.tool.large.PaginationRequestValidator; import io.shinhanlife.dap.mcg.tool.large.PaginationRequestValidator;
import io.shinhanlife.dat.mcg.tool.result.ToolExecutionResultFormatter; import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResultFormatter;
import io.shinhanlife.dat.mcg.tool.result.ToolExecutionResult; import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
import io.shinhanlife.dat.mcg.guardrail.ToolResponseGuardrailService; import io.shinhanlife.dap.mcg.guardrail.ToolResponseGuardrailService;
import io.shinhanlife.dat.mcg.transport.ToolInvoker; import io.shinhanlife.dap.mcg.transport.ToolInvoker;
@Slf4j @Slf4j
@Service @Service
@@ -62,7 +62,7 @@ public class ExecuteService {
private final AuditLogService auditLogService; private final AuditLogService auditLogService;
private final CircuitBreakerService circuitBreakerService; private final CircuitBreakerService circuitBreakerService;
private final ToolAuthorizationService authorizationService; private final ToolAuthorizationService authorizationService;
private final InMemoryToolTraceService traceService; private final RedisToolTraceService redisTrace;
private final McpGatewayProperties properties; private final McpGatewayProperties properties;
private final LargeToolResponseService largeResponses; private final LargeToolResponseService largeResponses;
private final PaginationRequestValidator paginationValidator; private final PaginationRequestValidator paginationValidator;
@@ -79,7 +79,7 @@ public class ExecuteService {
AuditLogService auditLogService, AuditLogService auditLogService,
CircuitBreakerService circuitBreakerService, CircuitBreakerService circuitBreakerService,
ToolAuthorizationService authorizationService, ToolAuthorizationService authorizationService,
InMemoryToolTraceService traceService, RedisToolTraceService redisTrace,
McpGatewayProperties properties, McpGatewayProperties properties,
LargeToolResponseService largeResponses, LargeToolResponseService largeResponses,
PaginationRequestValidator paginationValidator, PaginationRequestValidator paginationValidator,
@@ -94,7 +94,7 @@ public class ExecuteService {
this.auditLogService = auditLogService; this.auditLogService = auditLogService;
this.circuitBreakerService = circuitBreakerService; this.circuitBreakerService = circuitBreakerService;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.traceService = traceService; this.redisTrace = redisTrace;
this.properties = properties; this.properties = properties;
this.largeResponses = largeResponses; this.largeResponses = largeResponses;
this.paginationValidator = paginationValidator; this.paginationValidator = paginationValidator;
@@ -133,7 +133,7 @@ public class ExecuteService {
long startedAt = System.nanoTime(); long startedAt = System.nanoTime();
auditLogService.toolStarted(context, toolName, argumentsNode); auditLogService.toolStarted(context, toolName, argumentsNode);
traceService.started(context, metadata, argumentsNode); redisTrace.started(context, metadata, argumentsNode);
try { try {
authorizationService.authorize(context, metadata); authorizationService.authorize(context, metadata);
@@ -174,13 +174,13 @@ public class ExecuteService {
finalResult.put("original_size", originalSize); finalResult.put("original_size", originalSize);
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, ""); 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; return finalResult;
} catch (ToolExecutionException error) { } catch (ToolExecutionException error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name()); 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<>(); Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error"); errorResult.put("status", "error");
@@ -194,7 +194,7 @@ public class ExecuteService {
} catch (Exception error) { } catch (Exception error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name()); 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<>(); Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error"); errorResult.put("status", "error");
@@ -225,7 +225,7 @@ public class ExecuteService {
ToolExecutionException lastError = null; ToolExecutionException lastError = null;
for (int attempt = 1; attempt <= retryPolicy.maxAttempts(); attempt++) { for (int attempt = 1; attempt <= retryPolicy.maxAttempts(); attempt++) {
try { try {
traceService.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts()); redisTrace.attemptStarted(context, metadata, arguments, attempt, retryPolicy.maxAttempts());
circuitBreaker.beforeCall(); circuitBreaker.beforeCall();
Object result = executeOnce(context, metadata, arguments, payload); Object result = executeOnce(context, metadata, arguments, payload);
circuitBreaker.recordSuccess(); circuitBreaker.recordSuccess();
@@ -237,7 +237,7 @@ public class ExecuteService {
throw error; throw error;
} }
long backoffMillis = retryPolicy.backoffMillis(attempt); 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()); error.failureType().name());
sleepBeforeRetry(metadata.getName(), attempt, backoffMillis, error.failureType()); sleepBeforeRetry(metadata.getName(), attempt, backoffMillis, error.failureType());
} }
@@ -261,11 +261,6 @@ public class ExecuteService {
Map<String, String> headers = new java.util.HashMap<>(); Map<String, String> headers = new java.util.HashMap<>();
headers.put("trace-id", context.requestId()); headers.put("trace-id", context.requestId());
headers.put("request-id", java.util.UUID.randomUUID().toString()); 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); ObjectNode pageArguments = paginationValidator.normalize(metadata, arguments);
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId()); LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
@@ -342,18 +337,13 @@ public class ExecuteService {
if (!retryAllowedByOperation(metadata, arguments)) { if (!retryAllowedByOperation(metadata, arguments)) {
return RetryPolicy.disabled(); return RetryPolicy.disabled();
} }
return new RetryPolicy(retryMaxAttempts(metadata, properties.retryMaxAttempts()), properties.retryInitialBackoffMillis(), return new RetryPolicy(properties.retryMaxAttempts(), properties.retryInitialBackoffMillis(),
properties.retryBackoffMultiplier(), properties.retryMaxBackoffMillis()); 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) { private boolean retryAllowedByOperation(ToolMetadata metadata, ObjectNode arguments) {
if (metadata.getOperationType() == OperationType.READ) { if (metadata.getOperationType() == OperationType.READ) {
return true; return metadata.getRetryEnabled() != null ? metadata.getRetryEnabled() : true;
} }
return arguments.hasNonNull("idempotencyKey") && !arguments.path("idempotencyKey").asText("").isBlank(); 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; 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 * @className KillSwitchService
* @description AX HUB 시스템 위치 서비스 (인메모리) * @description AX HUB 시스템 처리 클래
* @author 0986406 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class KillSwitchService { public class KillSwitchService {
// 인메모리 맵으로 변경 // 기본 제공되는 StringRedisTemplate 사용
private final Map<String, String> killSwitchMap = new ConcurrentHashMap<>(); private final StringRedisTemplate stringRedisTemplate;
public void checkAgent(String tenantId) { public void checkAgent(String tenantId) {
String key = "mcp:kill:agent:" + tenantId; String key = "mcp:kill:agent:" + tenantId;
if ("true".equalsIgnoreCase(killSwitchMap.get(key))) { if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
log.warn(" [KillSwitch] 차단된 에이전트 접근 시도: {}", tenantId); log.warn(" [KillSwitch] 차단된 에이전트 접근 시도: {}", tenantId);
throw new SecurityException(" 비상 차단: 해당 테넌트(" + tenantId + ")의 접근이 관리자에 의해 차단되었습니다."); throw new SecurityException(" 비상 차단: 해당 테넌트(" + tenantId + ")의 접근이 관리자에 의해 차단되었습니다.");
} }
@@ -34,7 +39,7 @@ public class KillSwitchService {
if (toolName == null || toolName.isBlank()) return; if (toolName == null || toolName.isBlank()) return;
String key = "mcp:kill:tool:" + toolName; String key = "mcp:kill:tool:" + toolName;
if ("true".equalsIgnoreCase(killSwitchMap.get(key))) { if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
log.warn(" [KillSwitch] 차단된 툴 실행 시도: {}", toolName); log.warn(" [KillSwitch] 차단된 툴 실행 시도: {}", toolName);
throw new SecurityException(" 비상 차단: 해당 툴(" + toolName + ")의 실행이 관리자에 의해 차단되었습니다."); throw new SecurityException(" 비상 차단: 해당 툴(" + toolName + ")의 실행이 관리자에 의해 차단되었습니다.");
} }
@@ -44,7 +49,7 @@ public class KillSwitchService {
if (integrationType == null || integrationType.isBlank()) return; if (integrationType == null || integrationType.isBlank()) return;
String key = "mcp:kill:route:" + integrationType; String key = "mcp:kill:route:" + integrationType;
if ("true".equalsIgnoreCase(killSwitchMap.get(key))) { if ("true".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
log.warn(" [KillSwitch] 차단된 라우트 통신 시도: {}", integrationType); log.warn(" [KillSwitch] 차단된 라우트 통신 시도: {}", integrationType);
throw new SecurityException(" 비상 차단: 해당 레거시 라우트(" + integrationType + ")로의 통신이 관리자에 의해 차단되었습니다."); throw new SecurityException(" 비상 차단: 해당 레거시 라우트(" + integrationType + ")로의 통신이 관리자에 의해 차단되었습니다.");
} }
@@ -53,14 +58,14 @@ public class KillSwitchService {
// 관리자 API용 토글 메서드 // 관리자 API용 토글 메서드
public void toggleKillSwitch(String type, String target, boolean state) { public void toggleKillSwitch(String type, String target, boolean state) {
String key = "mcp:kill:" + type + ":" + target; 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); log.info(" [KillSwitch] 상태 변경: {} -> {} (차단 상태: {})", type, target, state);
} }
// 관리자 API용 삭제 메서드 ( 항목 삭제) // 관리자 API용 삭제 메서드 (Redis 자체를 완전 삭제)
public void removeKillSwitch(String type, String target) { public void removeKillSwitch(String type, String target) {
String key = "mcp:kill:" + type + ":" + target; String key = "mcp:kill:" + type + ":" + target;
killSwitchMap.remove(key); stringRedisTemplate.delete(key);
log.info(" [KillSwitch] 차단 키 완전 삭제: {} -> {}", type, target); 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.dap.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dat.mcg.registry.InMemoryRegistryService; import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dat.mcc.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dat.mcg.config.GatewayFallbackProperties; import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -12,7 +12,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* @package io.shinhanlife.dat.mcg.service * @package io.shinhanlife.dap.mcg.service
* @className ToolPlanner * @className ToolPlanner
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @author 0986406
@@ -30,7 +30,7 @@ import java.util.Map;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ToolPlanner { public class ToolPlanner {
private final InMemoryRegistryService InMemoryRegistryService; private final RedisRegistryService redisRegistryService;
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final GatewayFallbackProperties fallbackProperties; private final GatewayFallbackProperties fallbackProperties;
@@ -50,7 +50,7 @@ public class ToolPlanner {
// 2. RedisRegistry에서 해당 툴의 메타데이터 조회 // 2. RedisRegistry에서 해당 툴의 메타데이터 조회
// (실제로는 메타데이터가 실행 계획의 핵심이 됩니다) // (실제로는 메타데이터가 실행 계획의 핵심이 됩니다)
var toolMetadata = InMemoryRegistryService.getToolByName(toolName); var toolMetadata = redisRegistryService.getToolByName(toolName);
if (toolMetadata == null) { if (toolMetadata == null) {
log.warn(" [Planner] 등록되지 않은 툴 요청: {}. Fallback 라우팅 규칙을 확인합니다.", toolName); log.warn(" [Planner] 등록되지 않은 툴 요청: {}. Fallback 라우팅 규칙을 확인합니다.", toolName);
@@ -58,7 +58,7 @@ public class ToolPlanner {
String fallbackPodUrl = fallbackProperties.getDefaultUrl(); String fallbackPodUrl = fallbackProperties.getDefaultUrl();
if (fallbackProperties.getRoutes() != null) { if (fallbackProperties.getRoutes() != null) {
for (Map.Entry<String, String> entry : fallbackProperties.getRoutes().entrySet()) { for (Map.Entry<String, String> entry : fallbackProperties.getRoutes().entrySet()) {
if (toolName.startsWith(entry.getKey() + "_")) { if (toolName.contains(entry.getKey())) {
fallbackPodUrl = entry.getValue(); fallbackPodUrl = entry.getValue();
break; break;
} }

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

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 * @className DynamicMcpController
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 0986406 * @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.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import io.shinhanlife.dat.mcg.sync.CustomWebMvcSseServerTransportProvider; import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
@RestController @RestController
public class DynamicMcpController { public class DynamicMcpController {

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